@michaelthielemann/kestrel 4.1.3 → 5.0.2

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 (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -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 +162 -0
  15. package/dist/defineConfig.js +42 -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/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. package/templates/starter/tsconfig.json +0 -3
@@ -1,170 +0,0 @@
1
- import { readdirSync } from 'node:fs'
2
- import { basename, join } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import { blockNameFromFile } from './extract-block'
5
-
6
- type Lister = (dir: string) => string[]
7
-
8
- /**
9
- * Resolves a bare `@michaelthielemann/kestrel-*` package specifier to its real entry FILE PATH, via Node's
10
- * own ESM resolution from THIS module's own location — not the bare specifier itself, which a Nitro
11
- * virtual module (no real file path of its own) cannot resolve reliably: Rollup falls back to resolving a
12
- * virtual's imports from the project root, and under pnpm's isolated `node_modules` a package the
13
- * CONSUMER never declared a direct dependency on (only the engine did, transitively) is invisible there —
14
- * observed as `Cannot find package` at runtime, npm's flat hoisting having accidentally masked it in
15
- * testing. This module's own file, once installed, sits inside the engine's package tree, so resolving
16
- * from here walks the engine's OWN nested `node_modules`, where pnpm DID link the real dependency.
17
- */
18
- export function resolvePackageEntry(spec: string): string {
19
- return fileURLToPath(import.meta.resolve(spec))
20
- }
21
-
22
- const isDefinition = (name: string) =>
23
- name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.endsWith('.d.ts')
24
-
25
- export function listDefinitionFiles(dir: string, list: Lister = readdirSync): string[] {
26
- let entries: string[]
27
- try {
28
- entries = list(dir)
29
- } catch (err) {
30
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
31
- throw err
32
- }
33
- return entries
34
- .filter(isDefinition)
35
- .sort()
36
- .map((name) => join(dir, name))
37
- }
38
-
39
- /**
40
- * Definition files across every layer's `subdir`, deduped by BASENAME with LAYER PRIORITY: `layerRoots` is
41
- * Nuxt's `_layers` order (consumer/highest-priority first), so a consumer's `collections/pages.ts` (or
42
- * `field-types/color.ts`) SHADOWS the same-named lower-layer file — a deterministic override, instead of
43
- * importing both and relying on the registry's last-write-wins under an alphabetical path sort (which made
44
- * "who wins" a coincidence of the node_modules path). Mirrors `collectBlockSfcs`'s name-first dedup.
45
- */
46
- export function collectDefinitions(layerRoots: string[], subdir: string, list?: Lister): string[] {
47
- const byBasename = new Map<string, string>()
48
- for (const root of layerRoots) {
49
- for (const file of listDefinitionFiles(join(root, subdir), list)) {
50
- const name = basename(file)
51
- if (!byBasename.has(name)) byBasename.set(name, file) // first (highest-priority layer) wins
52
- }
53
- }
54
- return [...byBasename.values()].sort()
55
- }
56
-
57
- const isVue = (name: string) => name.endsWith('.vue') && !name.endsWith('.test.vue')
58
-
59
- /** `.vue` files in `dir`, sorted; `[]` when the dir is absent. Mirrors `listDefinitionFiles` for blocks. */
60
- export function listVueFiles(dir: string, list: Lister = readdirSync): string[] {
61
- let entries: string[]
62
- try {
63
- entries = list(dir)
64
- } catch (err) {
65
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
66
- throw err
67
- }
68
- return entries.filter(isVue).sort().map((name) => join(dir, name))
69
- }
70
-
71
- /** Block SFCs across every layer's `app/blocks/` dir, deduped by BLOCK NAME with LAYER PRIORITY: `layerRoots`
72
- * is Nuxt's `_layers` order (consumer/highest-priority first), so the first file seen for a given block name
73
- * wins. This matches how the display component is resolved (project-first), so a same-named override takes
74
- * its schema AND its template from the same layer — a plain alphabetical path sort handed the schema to
75
- * whichever path sorted last (typically the node_modules layer), silently splitting the two. */
76
- export function collectBlockSfcs(layerRoots: string[], subdir = 'app/blocks', list?: Lister): string[] {
77
- const byName = new Map<string, string>()
78
- for (const root of layerRoots) {
79
- for (const file of listVueFiles(join(root, subdir), list)) {
80
- const name = blockNameFromFile(basename(file))
81
- if (!byName.has(name)) byName.set(name, file) // first (highest-priority layer) wins
82
- }
83
- }
84
- // Name-unique now, so the final sort is only for a deterministic, stable import order.
85
- return [...byName.values()].sort()
86
- }
87
-
88
- /**
89
- * Each layer's OWN `server/db/manifest.ts` (ADR-0012 ownership manifest), one per layer that has one.
90
- * Unlike `collectDefinitions`, this does NOT dedupe by basename: every layer's manifest is a distinct
91
- * module contribution (no shadow/override semantics — `media`'s manifest and `public`'s manifest both
92
- * happen to be named `manifest.ts`, and both must survive). Layer order is whatever `layerRoots` is.
93
- */
94
- export function collectManifestFiles(layerRoots: string[], list?: Lister): string[] {
95
- const files: string[] = []
96
- for (const root of layerRoots) {
97
- for (const file of listDefinitionFiles(join(root, 'server/db'), list)) {
98
- if (basename(file) === 'manifest.ts') files.push(file)
99
- }
100
- }
101
- return files
102
- }
103
-
104
- export function renderRegistry(absPaths: string[]): string {
105
- if (!absPaths.length) return 'export default []'
106
- const imports = absPaths.map((p, i) => `import _${i} from ${JSON.stringify(p)}`)
107
- const arr = absPaths.map((_, i) => `_${i}`).join(', ')
108
- return `${imports.join('\n')}\nexport default [${arr}]`
109
- }
110
-
111
- /**
112
- * A registry combining package-provided items (each package's `kestrelDiscovery.<property>`, read via a
113
- * bare specifier — no filesystem/`node_modules` guessing) with consumer/layer-scanned files, with a
114
- * SAME-NAME layer item overriding a package's (via `mergeKestrelDiscovered`, evaluated at virtual-load
115
- * time — package names aren't knowable at codegen time without loading the package, which this
116
- * deliberately avoids doing at build time). `nameOfExpr` is a JS arrow-function source string the
117
- * generated module evaluates per item (e.g. `'(x) => x.name'` for collections,
118
- * `'(x) => __kestrelTableName(x)'` for schema-tables, paired with `extraImports` supplying
119
- * `__kestrelTableName`).
120
- */
121
- export function renderPackageMergedRegistry(opts: {
122
- packages: string[]
123
- property: 'collections' | 'schemaTables'
124
- consumerFiles: string[]
125
- nameOfExpr: string
126
- extraImports?: string
127
- preamble?: string
128
- }): string {
129
- const pkgImports = opts.packages.map((spec, i) => `import { kestrelDiscovery as __pkg${i} } from ${JSON.stringify(spec)}`)
130
- const pkgSpread = opts.packages.map((_, i) => `...(__pkg${i}.${opts.property} ?? [])`).join(', ')
131
- const consumerImports = opts.consumerFiles.map((p, i) => `import _c${i} from ${JSON.stringify(p)}`)
132
- const consumerArr = opts.consumerFiles.map((_, i) => `_c${i}`).join(', ')
133
- return [
134
- opts.preamble ?? '',
135
- opts.extraImports ?? '',
136
- `import { mergeKestrelDiscovered } from ${JSON.stringify(resolvePackageEntry('@michaelthielemann/kestrel-core'))}`,
137
- ...pkgImports,
138
- ...consumerImports,
139
- `export default mergeKestrelDiscovered([${pkgSpread}], [${consumerArr}], ${opts.nameOfExpr})`,
140
- ].filter(Boolean).join('\n')
141
- }
142
-
143
- /**
144
- * A registry concatenating package-provided items (`kestrelDiscovery.<property>`) with consumer/layer-
145
- * scanned files — NO dedup (mirrors `collectManifestFiles`'s own "every contribution is distinct, no
146
- * shadow semantics" contract; unlike collections/schema-tables, two manifests sharing a name is not a
147
- * schema conflict, so there is nothing to override).
148
- */
149
- export function renderPackageConcatRegistry(opts: {
150
- packages: string[]
151
- property: 'manifest'
152
- consumerFiles: string[]
153
- preamble?: string
154
- }): string {
155
- const pkgImports = opts.packages.map((spec, i) => `import { kestrelDiscovery as __pkg${i} } from ${JSON.stringify(spec)}`)
156
- // `opts.packages` is expected to be pre-filtered to packages that actually contribute this property (the
157
- // caller's per-category list), so every `.manifest` read here SHOULD be defined — but the spread stays
158
- // conditional (`...(x ? [x] : [])`, not a bare `x`) as a defense-in-depth guard: a package mis-listed in
159
- // the wrong `PACKAGE_*` category contributes nothing instead of polluting the array with `undefined`.
160
- const pkgItems = opts.packages.map((_, i) => `...(__pkg${i}.${opts.property} ? [__pkg${i}.${opts.property}] : [])`)
161
- const consumerImports = opts.consumerFiles.map((p, i) => `import _c${i} from ${JSON.stringify(p)}`)
162
- const consumerArr = opts.consumerFiles.map((_, i) => `_c${i}`)
163
- const all = [...pkgItems, ...consumerArr].join(', ')
164
- return [
165
- opts.preamble ?? '',
166
- ...pkgImports,
167
- ...consumerImports,
168
- `export default [${all}]`,
169
- ].filter(Boolean).join('\n')
170
- }
@@ -1,20 +0,0 @@
1
- declare module '#kestrel/collections' {
2
- const collections: unknown[]
3
- export default collections
4
- }
5
- declare module '#kestrel/blocks' {
6
- const blocks: unknown[]
7
- export default blocks
8
- }
9
- declare module '#kestrel/schema-tables' {
10
- const tables: unknown[]
11
- export default tables
12
- }
13
- declare module '#kestrel/module-manifests' {
14
- import type { OwnershipManifest } from '@michaelthielemann/kestrel-contracts'
15
- const manifests: OwnershipManifest[]
16
- export default manifests
17
- }
18
- declare module '#build/kestrel-layouts.mjs' {
19
- export const kestrelLayouts: string[]
20
- }
@@ -1,44 +0,0 @@
1
- import { addComponentsDir, defineNuxtModule } from '@nuxt/kit'
2
- import { resolve } from 'node:path'
3
- import { KESTREL_COMPONENT_PREFIX, KESTREL_OVERRIDE_PRIORITY } from './shared'
4
-
5
- /** Where a consumer places a component that replaces a shipped one, relative to their `srcDir`. */
6
- export const KESTREL_OVERRIDE_DIR = 'Kestrel/components'
7
-
8
- /**
9
- * RULING: the app-side layers (`admin`, `ui`, and the `app/` halves of `media`/`public`) stay LAYERS —
10
- * they are not bridged into packages, and there is no `addComponentsDir`/`addImportsDir`
11
- * module doing so. `admin` is the layer with the most at stake here: it registers by far the most
12
- * components under `kestrelComponents()` (`layers/admin/nuxt.config.ts`, alongside `media`/`public`/`ui`),
13
- * so it is the layer this override mechanism protects most. This module's own override mechanism (below)
14
- * is WHY: it depends on Kestrel's own components being real, Nuxt-scannable `.vue` files in a real layer,
15
- * resolved at a higher `addComponentsDir` priority than a consumer's override — a package's compiled
16
- * `dist` output cannot participate in that priority system the same way. Bridging would cost the admin its
17
- * per-component override story for no boot-order/discovery benefit these app-side files need (none of the
18
- * eager-module-load or field-type-registration hazards the *server*-side package cut existed to fix apply
19
- * here). Revisit only with strong evidence the cost is worth paying — see `docs/internals/layers-and-packages.md` §
20
- * "The component/composable bridge".
21
- *
22
- * Composables are a separate question this mechanism does NOT cover: none of `admin`/`ui`'s ~30
23
- * `use*`-named composables carry a `useKestrel*` prefix, and none need to — every one is internal editor
24
- * state/logic (`useEditForm`, `useBlockTree`, `useCollectionOps`, …). Kestrel's own layers reuse them
25
- * freely across the merged auto-import namespace (`media` uses `ui`'s `useT`/`useToast`/`useDragReorder`);
26
- * what stays true is that no extension, package, or third-party consumer imports one, and none is ever
27
- * documented as something a consumer reaches for — a consumer builds their own UI against collections/
28
- * pipelines/field types, not by importing Kestrel's editor internals. There is consequently no override
29
- * mechanism for composables (unlike components) and none is needed.
30
- */
31
-
32
- export default defineNuxtModule({
33
- meta: { name: 'kestrel-component-namespace' },
34
- setup(_options, nuxt) {
35
- // The trailing `components` segment is load-bearing: Nuxt reports a missing registered directory
36
- // (NUXT_B3001) unless the path matches its default-components pattern, which requires that suffix.
37
- // Without it every consumer who never uses the seam would see a build warning.
38
- addComponentsDir({
39
- path: resolve(nuxt.options.srcDir, KESTREL_OVERRIDE_DIR),
40
- prefix: KESTREL_COMPONENT_PREFIX,
41
- priority: KESTREL_OVERRIDE_PRIORITY,
42
- })
43
- },
44
- })
@@ -1,24 +0,0 @@
1
- import { fileURLToPath } from 'node:url'
2
-
3
- export const KESTREL_COMPONENT_PREFIX = 'Kestrel'
4
-
5
- /**
6
- * Above every layer-derived priority (Nuxt assigns `layerCount - i`, so the consumer's app dir — index 0
7
- * — otherwise always wins). A consumer file that lands on a `Kestrel*` name anywhere but the override
8
- * directory therefore loses instead of silently replacing the admin component.
9
- */
10
- export const KESTREL_LAYER_PRIORITY = 100
11
-
12
- /** The one supported override seam, above the layers. */
13
- export const KESTREL_OVERRIDE_PRIORITY = 200
14
-
15
- /**
16
- * The components directory of a Kestrel layer, namespaced. Nuxt drops a prefix the filename already
17
- * carries, so `KestrelImg.vue` stays `KestrelImg` while `ui/Button.vue` becomes `KestrelUiButton`.
18
- * Pass the layer's own `import.meta.url`.
19
- */
20
- export const kestrelComponents = (layerUrl: string) => ({
21
- path: fileURLToPath(new URL('./app/components', layerUrl)),
22
- prefix: KESTREL_COMPONENT_PREFIX,
23
- priority: KESTREL_LAYER_PRIORITY,
24
- })
@@ -1,55 +0,0 @@
1
- export type AppShellDiagnostic = { level: 'error' | 'warn'; message: string }
2
-
3
- export type AppShellInput = {
4
- mainComponent: string | null | undefined
5
- pagesEnabled: boolean
6
- read: (file: string) => string
7
- }
8
-
9
- const withoutComments = (src: string) => src.replace(/<!--[\s\S]*?-->/g, '')
10
-
11
- // `<nuxt-page />` is as valid as `<NuxtPage />`; matching only the Pascal spelling would flag a working app.
12
- const kebab = (tag: string) => tag.replace(/(?!^)([A-Z])/g, '-$1').toLowerCase()
13
- const uses = (src: string, tag: string) => new RegExp(`<\\s*(${tag}|${kebab(tag)})[\\s/>]`, 'i').test(src)
14
-
15
- const FIX = `
16
- <template>
17
- <NuxtLayout>
18
- <NuxtPage />
19
- </NuxtLayout>
20
- </template>`
21
-
22
- /** Reports only — assigning `mainComponent` would defeat a legitimate override. See ADR-0005. */
23
- export function diagnoseAppShell({ mainComponent, pagesEnabled, read }: AppShellInput): AppShellDiagnostic[] {
24
- const found: AppShellDiagnostic[] = []
25
-
26
- if (!pagesEnabled) {
27
- found.push({
28
- level: 'error',
29
- message:
30
- 'the pages feature is disabled (`pages: false`), so no route is registered at all — the admin is a set of pages under `app/pages/admin/`. Remove the override to reach /admin.',
31
- })
32
- }
33
-
34
- if (mainComponent) {
35
- let src: string | undefined
36
- try {
37
- src = withoutComments(read(mainComponent))
38
- } catch {
39
- // An unreadable app root is Nuxt's to report; guessing here would produce a phantom error.
40
- }
41
- if (src !== undefined && !uses(src, 'NuxtPage')) {
42
- found.push({
43
- level: 'error',
44
- message: `${mainComponent} renders no <NuxtPage />, so NO route renders — including /admin. It shadows Kestrel's own app.vue because the consumer layer wins. Either delete it (Kestrel ships a working one) or make it:${FIX}`,
45
- })
46
- } else if (src !== undefined && !uses(src, 'NuxtLayout')) {
47
- found.push({
48
- level: 'warn',
49
- message: `${mainComponent} renders no <NuxtLayout />, so page-level \`layout\` is ignored and the admin loses its navigation shell. Wrap <NuxtPage /> in <NuxtLayout>.`,
50
- })
51
- }
52
- }
53
-
54
- return found
55
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Kestrel's authoring API, registered as Nuxt/Nitro auto-imports so a site built on Kestrel can write
3
- * `defineCollection({...})` or `getCollection('pages')` without importing — the same surface a 3.x site
4
- * had when this code still lived in the layers' `server/utils`. The names are read off the packages'
5
- * real exports at build time, so a new public function is auto-importable the moment it is exported;
6
- * every name stays importable explicitly from its package as well.
7
- */
8
- export interface AutoImportEntry {
9
- name: string
10
- from: string
11
- }
12
-
13
- const PKG = '@michaelthielemann/kestrel-'
14
-
15
- /** Order matters: a name exported by several packages (re-exports) is registered from the first one. */
16
- const SERVER_PACKAGES = ['core', 'fields', 'contracts', 'auth', 'access', 'media', 'collections', 'publishing', 'delivery-live', 'delivery-static']
17
- const APP_PACKAGES = ['core/client', 'fields/client']
18
-
19
- /** `kestrelDiscovery`: module manifests for the discovery module, never something a site calls.
20
- * `renderRouteLive`: the public layer's own `server/utils` wrapper of the same name is the wired one, and
21
- * Nitro's directory scan already auto-imports it — registering the package export too only draws a
22
- * "duplicated imports" warning on every build. */
23
- const EXCLUDED = new Set(['default', 'kestrelDiscovery', 'renderRouteLive'])
24
-
25
- async function collect(packages: string[]): Promise<AutoImportEntry[]> {
26
- const seen = new Set<string>()
27
- const entries: AutoImportEntry[] = []
28
- for (const pkg of packages) {
29
- const from = PKG + pkg
30
- const mod = (await import(from)) as Record<string, unknown>
31
- for (const name of Object.keys(mod)) {
32
- if (EXCLUDED.has(name) || seen.has(name)) continue
33
- seen.add(name)
34
- entries.push({ name, from })
35
- }
36
- }
37
- return entries
38
- }
39
-
40
- /** Server-only (Nitro) authoring API: every runtime export of the server packages. */
41
- export const serverAutoImports = (): Promise<AutoImportEntry[]> => collect(SERVER_PACKAGES)
42
-
43
- /** App-side (Vue) authoring API: every runtime export of the client entry points. */
44
- export const appAutoImports = (): Promise<AutoImportEntry[]> => collect(APP_PACKAGES)
@@ -1,144 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { defineNuxtModule } from '@nuxt/kit'
3
- import { resolveKestrel, type KestrelConfig } from '@michaelthielemann/kestrel-core'
4
- import { diagnoseAppShell } from './app-shell'
5
- import { appAutoImports, serverAutoImports } from './auto-imports'
6
-
7
- /**
8
- * The `kestrel` config namespace (`kestrel: { … }` in nuxt.config, sourced from `kestrel.config.ts`).
9
- * Lives in `core` so it runs before the `media`/`public` layer modules and populates runtimeConfig
10
- * first. It bridges the resolved config to what the CLIENT + Nitro need: `runtimeConfig.media.*`,
11
- * `runtimeConfig.public.{locales,primaryLocale}`, and the `/uploads` static serving. Server runtime
12
- * utils (db/locale/site-url) read the same resolved values directly via `server/utils/kestrel-config.ts`.
13
- */
14
- export default defineNuxtModule<KestrelConfig>({
15
- meta: { name: 'kestrel', configKey: 'kestrel' },
16
- async setup(options, nuxt) {
17
- const c = resolveKestrel(options, process.env, nuxt.options.rootDir)
18
-
19
- // Registered through hooks rather than `addImports`/`addServerImports`, which need an active Kit
20
- // instance (`useNuxt()`) that plain `setup()` callers (tests) do not provide.
21
- const [app, server] = await Promise.all([appAutoImports(), serverAutoImports()])
22
- // The packages' own compiled output must not be rewritten by the auto-import transform: it would
23
- // inject an import for a symbol a dist file declares itself. Only the `kestrel-*` packages are
24
- // excluded — never `node_modules` as a whole: in a consumer the engine's own layers live there too,
25
- // and their server plugins rely on the transform for Nitro's `defineNitroPlugin` & co.
26
- const ownDist = /[\\/](node_modules|packages)[\\/](@michaelthielemann[\\/])?kestrel-[^\\/]+[\\/]/
27
- const imports = (nuxt.options.imports ||= {} as typeof nuxt.options.imports)
28
- imports.transform ||= {}
29
- imports.transform.exclude = [...(imports.transform.exclude ?? []), ownDist]
30
- nuxt.hook('imports:extend', (imports) => {
31
- imports.push(...app)
32
- })
33
- nuxt.hook('nitro:config', (config) => {
34
- config.imports ||= {}
35
- config.imports.imports ||= []
36
- config.imports.imports.push(...server)
37
- const prior = config.imports.exclude
38
- const excluded = Array.isArray(prior) ? [...prior] : prior ? [prior] : []
39
- config.imports.exclude = [...excluded, ownDist]
40
- })
41
-
42
- // `app:resolve` is the first hook where `mainComponent` is settled across all layers. In dev it fires
43
- // on every watched change, so an unfixed problem would repaint the whole message on each keystroke.
44
- const reported = new Set<string>()
45
- nuxt.hook('app:resolve', (app) => {
46
- for (const d of diagnoseAppShell({
47
- mainComponent: app.mainComponent,
48
- pagesEnabled: nuxt.options.pages !== false,
49
- read: (file) => readFileSync(file, 'utf8'),
50
- })) {
51
- if (reported.has(d.message)) continue
52
- reported.add(d.message)
53
- console[d.level === 'error' ? 'error' : 'warn'](`[kestrel] ${d.message}`)
54
- }
55
- })
56
-
57
- const rc = nuxt.options.runtimeConfig
58
- rc.media = {
59
- driver: c.media.driver,
60
- maxUploadBytes: c.media.maxUploadBytes,
61
- allowedMimes: c.media.allowedMimes,
62
- imagePolicy: c.media.imagePolicy,
63
- local: { dir: c.media.dir, baseUrl: c.media.baseUrl },
64
- // Non-secret S3 settings come from the resolved config; the credentials are env-only (never
65
- // committed) and merged in here so the server driver gets everything from runtimeConfig.
66
- s3: {
67
- ...c.media.s3,
68
- accessKeyId: process.env.KESTREL_S3_ACCESS_KEY_ID ?? '',
69
- secretAccessKey: process.env.KESTREL_S3_SECRET_ACCESS_KEY ?? '',
70
- sessionToken: process.env.KESTREL_S3_SESSION_TOKEN ?? '',
71
- },
72
- }
73
- const pub = (rc.public ??= {} as never) as Record<string, unknown>
74
- pub.locales = c.supportedLocales
75
- pub.primaryLocale = c.primaryLocale
76
- pub.prefixPrimary = c.prefixPrimary
77
- // The public head (canonical/og:url/hreflang) needs the absolute origin CLIENT-side too (SPA navs
78
- // update the head after hydration). Not a secret — it is the site's own public address.
79
- pub.siteUrl = c.siteUrl ?? ''
80
- pub.siteName = c.siteName ?? ''
81
- // Reference desktop width (px) for the admin page-builder preset; the editor's scale-to-fit shrinks it.
82
- pub.previewDesktopWidth = c.preview.desktopWidth
83
- // Whether the admin shows the EU AI Act disclosure controls on a media asset; the data itself is
84
- // always resolved server-side, so flipping this off only hides the editor.
85
- pub.aiDisclosureEnabled = c.aiDisclosure.enabled
86
- // Article metadata is an APP-side concern on both ends — the admin SEO panel decides whether to offer
87
- // the fields, the public page decides whether to publish them as JSON-LD — so the flag has to be
88
- // client-visible. It gates a disclosure, not a secret.
89
- pub.seoArticleMeta = c.seo.articleMeta
90
-
91
- // Server-only resolved settings, so server utils read the CONSUMER's `kestrel: {}` (via this module)
92
- // rather than importing Kestrel's own `kestrel.config.ts` file. Essential when Kestrel is consumed as
93
- // a layer: the package's config file is empty, the real config lives in the consumer's nuxt.config.
94
- const srv = (rc.kestrel ??= {} as never) as Record<string, unknown>
95
- srv.dbPath = c.dbPath
96
- srv.siteUrl = c.siteUrl
97
- srv.siteName = c.siteName
98
- srv.siteDescription = c.siteDescription
99
- // Built-in collection toggles (pages/media) — read by the register plugin to gate built-ins.
100
- srv.collections = c.collections
101
- // Read by the upload route to decide whether to run the AI-signal scan at all.
102
- srv.aiDisclosure = c.aiDisclosure
103
- // Answer-engine toggles; `llmsFull` is server-only (it gates a Nitro route and what the publisher writes).
104
- srv.seo = c.seo
105
- // Read by delivery-live's catch-all/read pipeline (see `KestrelConfig.delivery`'s own TSDoc) to
106
- // decide whether to answer at all — everything else about it stays 'static'-shaped either way.
107
- srv.delivery = c.delivery
108
- // Extra path prefixes the live catch-all must fall through on (see `KestrelConfig.deliveryExempt`).
109
- srv.deliveryExempt = c.deliveryExempt
110
- // Static-publish target for the runtime publisher (server-only). S3 creds are env-only; prefer the
111
- // output-specific creds, fall back to the shared media creds so a single S3 account "just works".
112
- srv.output = {
113
- driver: c.output.driver,
114
- dir: c.output.dir,
115
- publicDir: c.output.publicDir,
116
- auto: c.output.auto,
117
- publishOnSave: c.output.publishOnSave,
118
- reconcileMinutes: c.output.reconcileMinutes,
119
- verbose: c.output.verbose,
120
- s3: {
121
- ...c.output.s3,
122
- accessKeyId: process.env.KESTREL_OUTPUT_S3_ACCESS_KEY_ID ?? process.env.KESTREL_S3_ACCESS_KEY_ID ?? '',
123
- secretAccessKey: process.env.KESTREL_OUTPUT_S3_SECRET_ACCESS_KEY ?? process.env.KESTREL_S3_SECRET_ACCESS_KEY ?? '',
124
- sessionToken: process.env.KESTREL_OUTPUT_S3_SESSION_TOKEN ?? process.env.KESTREL_S3_SESSION_TOKEN ?? '',
125
- },
126
- }
127
-
128
- nuxt.options.nitro ??= {}
129
- // Only the local driver serves uploads off disk from the app origin (dev + baked into `nuxt
130
- // generate`). On S3 the bucket/CDN serves them, so we must not register a publicAssets entry for
131
- // a directory that isn't the store (it would 404 or shadow real keys).
132
- if (c.media.driver === 'local') {
133
- nuxt.options.nitro.publicAssets = [
134
- ...(nuxt.options.nitro.publicAssets ?? []),
135
- { baseURL: c.media.baseUrl, dir: c.media.dir, maxAge: 0 },
136
- ]
137
- // Uploads are served from the app origin; never let the browser content-sniff past the declared type.
138
- nuxt.options.nitro.routeRules = {
139
- ...(nuxt.options.nitro.routeRules ?? {}),
140
- [`${c.media.baseUrl}/**`]: { headers: { 'X-Content-Type-Options': 'nosniff' } },
141
- }
142
- }
143
- },
144
- })
@@ -1,69 +0,0 @@
1
- import { defineNuxtModule } from '@nuxt/kit'
2
- import { assertEffectiveOrderMatches, resolvePluginOrder, validatePluginOrder } from './plugin-order'
3
-
4
- /**
5
- * Owns the execution order of the plugins Kestrel's own 6 layers ship (core/fields/media/auth/
6
- * collections/public — see `plugin-order.ts`'s `PLUGIN_ORDER`), replacing Nitro's layer-then-filename
7
- * sort with an explicit, declared list. A consumer's own plugins (and any extension layer's) are
8
- * untouched — they keep using Nitro's default scan-and-append, landing wherever their own layer position
9
- * in `extends` naturally puts them; this module only orders Kestrel's own.
10
- *
11
- * Mechanism: the `nitro:config` hook's config object (which becomes `nitro.options` once Nitro's own
12
- * instance is built) gets `.plugins` pushed with `PLUGIN_ORDER`'s resolved absolute paths, in order — this
13
- * hook runs BEFORE Nitro's own `scanPlugins` step. That step only appends a scanned file when it is not
14
- * ALREADY in `nitro.options.plugins`
15
- * (`nitropack/dist/core/index.mjs`: `if (!nitro.options.plugins.includes(plugin)) plugins.push(plugin)`),
16
- * so every one of our explicitly-pushed paths is skipped by the scan (already present, same absolute path)
17
- * and keeps our declared position — nothing needs renaming or moving on disk. `nitro:config` alone only
18
- * proves what WE push, though — a separate `nitro:init` hook (which fires after every `nitro:config` hook
19
- * and Nitro's own scan-and-append have both already run, against the fully-built Nitro instance) asserts
20
- * the REAL, final `nitro.options.plugins` still starts with our declared order, contiguous and duplicate-
21
- * free — see `assertEffectiveOrderMatches`'s own TSDoc for why this observer, not the push alone, is what
22
- * actually proves "declared order ≡ effective order".
23
- *
24
- * The ADR-0029 eager-barrel-load guard for a future `00.*` plugin: NOT a mechanism in this module. It
25
- * doesn't need to be — `@michaelthielemann/kestrel-media`/`@michaelthielemann/kestrel-collections`/`@michaelthielemann/kestrel-publishing`'s own barrels now
26
- * self-guard (a used-binding `@michaelthielemann/kestrel-fields` import, first), so importing ANY of them is safe from ANY
27
- * plugin position, including a hypothetical new `00.*` file. The computed rail proving this
28
- * (`test/architecture/kestrel-discovery.test.ts`'s "every package whose OWN module graph reaches
29
- * buildCollection()...") already re-checks every package on every test run, so a FUTURE package that
30
- * starts calling `buildCollection()` without the guard fails there — independent of whatever plugin order
31
- * this module declares. Plugin order literally cannot reintroduce this hazard class any more, which is the
32
- * honest, verifiable version of "prevents a future 00.* plugin from eagerly loading a heavy barrel
33
- * pre-seed": the guard lives at the package boundary, not the boot-order boundary.
34
- */
35
- /** The minimal shape this module needs from `nuxt` — narrowed so a test can pass a plain mock object
36
- * instead of a real Nuxt instance (mirrors testing `defineNuxtModule`'s callback logic directly, not
37
- * through Nuxt's own module-loading machinery). */
38
- export interface MinimalNuxt {
39
- options: { _layers: readonly { cwd: string }[] }
40
- hook: {
41
- (name: 'nitro:config', fn: (nitro: { plugins?: (string | undefined)[] }) => void): void
42
- (name: 'nitro:init', fn: (nitro: { options: { plugins: (string | undefined)[] } }) => void): void
43
- }
44
- }
45
-
46
- /** The module's actual logic, exported separately from `defineNuxtModule`'s wrapper so a test can call it
47
- * against a plain mock `nuxt`/`nitro` pair and assert `nitro.plugins` ends up EXACTLY
48
- * `resolvePluginOrder(roots)`, in order — the "declared order ≡ effective order" proof, at unit speed,
49
- * independent of the real-boot proof (a real `nuxt build`, inspected directly). */
50
- export function setupPluginOrder(nuxt: MinimalNuxt): void {
51
- const roots = nuxt.options._layers.map((layer) => layer.cwd)
52
- validatePluginOrder(roots) // throws — fails the build loudly on any drift, before Nitro ever scans
53
-
54
- nuxt.hook('nitro:config', (nitro) => {
55
- nitro.plugins ||= []
56
- nitro.plugins.push(...resolvePluginOrder(roots))
57
- })
58
-
59
- nuxt.hook('nitro:init', (nitro) => {
60
- assertEffectiveOrderMatches(nitro.options.plugins, resolvePluginOrder(roots))
61
- })
62
- }
63
-
64
- export default defineNuxtModule({
65
- meta: { name: 'kestrel-plugin-order' },
66
- setup(_options, nuxt) {
67
- setupPluginOrder(nuxt)
68
- },
69
- })