@michaelthielemann/kestrel 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -110
- package/kestrel.config.ts +1 -1
- package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
- package/layers/access/server/middleware/access-guard.ts +25 -23
- package/layers/access/server/types.d.ts +1 -2
- package/layers/admin/app/components/AdminAccount.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +1 -2
- package/layers/admin/app/components/BlockPreview.vue +17 -5
- package/layers/admin/app/components/BlocksBody.vue +1 -1
- package/layers/admin/app/components/CollectionEditor.vue +35 -16
- package/layers/admin/app/components/CollectionList.vue +34 -3
- package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
- package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
- package/layers/admin/app/components/CollectionListPager.vue +1 -1
- package/layers/admin/app/components/CollectionListTable.vue +41 -2
- package/layers/admin/app/components/EditorStatus.vue +8 -7
- package/layers/admin/app/components/LocaleBar.vue +3 -1
- package/layers/admin/app/components/PageFields.vue +2 -4
- package/layers/admin/app/components/SeoFields.vue +2 -4
- package/layers/admin/app/components/SingletonEditor.vue +3 -2
- package/layers/admin/app/composables/useAuth.ts +3 -3
- package/layers/admin/app/composables/useBlockTree.ts +1 -1
- package/layers/admin/app/composables/useBlocks.ts +1 -1
- package/layers/admin/app/composables/useCollectionOps.ts +45 -8
- package/layers/admin/app/composables/useCollections.ts +1 -2
- package/layers/admin/app/composables/useEditForm.ts +35 -17
- package/layers/admin/app/composables/useLinkResolver.ts +1 -1
- package/layers/admin/app/composables/useListBatchActions.ts +20 -3
- package/layers/admin/app/composables/useListColumns.ts +1 -1
- package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
- package/layers/admin/app/composables/useListRows.ts +7 -2
- package/layers/admin/app/composables/useListUrlState.ts +1 -1
- package/layers/admin/app/composables/usePublishStatus.ts +2 -2
- package/layers/admin/app/layouts/admin.vue +0 -1
- package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
- package/layers/admin/app/pages/admin/index.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +2 -2
- package/layers/admin/app/utils/block-tree.ts +1 -1
- package/layers/admin/app/utils/collection-ops.ts +28 -3
- package/layers/admin/app/utils/edit-form.ts +34 -2
- package/layers/admin/app/utils/editor-expose.ts +2 -0
- package/layers/admin/app/utils/editor-form-context.ts +1 -2
- package/layers/admin/app/utils/field-keys.ts +1 -1
- package/layers/admin/app/utils/list-columns.ts +3 -2
- package/layers/admin/app/utils/list-query.ts +2 -2
- package/layers/admin/app/utils/populate-blocks.ts +2 -2
- package/layers/admin/app/utils/reauth.ts +5 -6
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
- package/layers/collections/server/plugins/01.register.ts +9 -12
- package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
- package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
- package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
- package/layers/core/modules/auto-discovery/index.ts +68 -11
- package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
- package/layers/core/modules/auto-discovery/scan.ts +77 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
- package/layers/core/modules/component-namespace/index.ts +24 -0
- package/layers/core/modules/kestrel/index.ts +6 -1
- package/layers/core/modules/plugin-order/index.ts +69 -0
- package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/api/[...path].ts +98 -0
- package/layers/core/server/plugins/00.config.ts +12 -0
- package/layers/core/server/plugins/00.migrate.ts +1 -0
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
- package/layers/core/server/plugins/02.schema-sync.ts +2 -7
- package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
- package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
- package/layers/core/server/tasks/db/migrate-module.ts +83 -0
- package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
- package/layers/core/server/tasks/db/migrate.ts +2 -6
- package/layers/core/server/utils/server-config.ts +66 -3
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
- package/layers/media/app/components/KestrelImg.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +2 -2
- package/layers/media/app/components/field/Media.vue +1 -1
- package/layers/media/app/composables/useMediaResolver.ts +1 -1
- package/layers/media/app/composables/useMediaUpload.ts +1 -1
- package/layers/media/app/composables/useMediaVariant.ts +2 -2
- package/layers/media/app/utils/ai-disclosure.ts +1 -1
- package/layers/media/app/utils/library.ts +1 -1
- package/layers/media/app/utils/ops.ts +1 -3
- package/layers/media/app/utils/picture-model.ts +2 -2
- package/layers/media/app/utils/variant-name.ts +1 -1
- package/layers/media/server/middleware/ondemand-variants.ts +6 -5
- package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
- package/layers/media/server/plugins/02.register-media.ts +10 -7
- package/layers/media/server/plugins/04.variant-capture.ts +12 -8
- package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
- package/layers/media/server/tasks/media/backfill.ts +3 -4
- package/layers/public/app/components/KestrelLink.vue +1 -1
- package/layers/public/app/pages/[...slug].vue +1 -1
- package/layers/public/app/pages/__kestrel/preview.vue +2 -2
- package/layers/public/app/utils/json-ld.ts +1 -1
- package/layers/public/app/utils/link-href.ts +1 -1
- package/layers/public/app/utils/page-head.ts +1 -1
- package/layers/public/modules/deploy-output/index.ts +4 -6
- package/layers/public/modules/prerender-routes/index.ts +4 -4
- package/layers/public/modules/prerender-routes/routes.ts +1 -1
- package/layers/public/modules/prune-media/index.ts +2 -2
- package/layers/public/server/delivery-live/serve.ts +111 -0
- package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
- package/layers/public/server/pipelines/index.ts +23 -0
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
- package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
- package/layers/public/server/plugins/02.register-links.ts +2 -3
- package/layers/public/server/plugins/03.redirects.ts +41 -23
- package/layers/public/server/plugins/05.plan-publish.ts +5 -0
- package/layers/public/server/plugins/zz.publish.ts +79 -24
- package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
- package/layers/public/server/routes/llms.txt.get.ts +4 -2
- package/layers/public/server/routes/redirects.json.get.ts +2 -2
- package/layers/public/server/routes/robots.txt.get.ts +2 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
- package/layers/public/server/tasks/publish/run.ts +9 -4
- package/layers/public/server/utils/publish/render-live.ts +26 -0
- package/layers/ui/app/components/field/Choice.vue +1 -1
- package/layers/ui/app/components/field/Datetime.vue +1 -1
- package/layers/ui/app/components/field/Json.vue +1 -2
- package/layers/ui/app/components/field/Layout.vue +2 -4
- package/layers/ui/app/components/field/Link.vue +1 -1
- package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
- package/layers/ui/app/components/field/Number.vue +2 -2
- package/layers/ui/app/components/field/Relation.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +6 -8
- package/layers/ui/app/components/field/Slug.vue +2 -3
- package/layers/ui/app/components/field/Text.vue +1 -1
- package/layers/ui/app/components/ui/Button.vue +2 -2
- package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
- package/layers/ui/app/components/ui/Checkbox.vue +2 -2
- package/layers/ui/app/components/ui/Combobox.vue +5 -5
- package/layers/ui/app/components/ui/DatePicker.vue +2 -2
- package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
- package/layers/ui/app/components/ui/NumberInput.vue +4 -4
- package/layers/ui/app/components/ui/Richtext.vue +1 -1
- package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
- package/layers/ui/app/components/ui/Select.vue +2 -2
- package/layers/ui/app/components/ui/TextInput.vue +4 -4
- package/layers/ui/app/components/ui/Textarea.vue +2 -2
- package/layers/ui/app/components/ui/TimeInput.vue +2 -2
- package/layers/ui/app/components/ui/Toasts.vue +3 -3
- package/layers/ui/app/composables/useLinkField.ts +1 -1
- package/layers/ui/app/composables/useRepeater.ts +1 -1
- package/layers/ui/app/i18n/de.ts +7 -7
- package/layers/ui/app/i18n/en.ts +7 -39
- package/layers/ui/app/utils/field-component.ts +1 -1
- package/layers/ui/app/utils/field-registry.ts +1 -1
- package/layers/ui/app/utils/icons.ts +2 -6
- package/layers/ui/app/utils/localized.ts +1 -1
- package/nuxt.config.ts +1 -1
- package/package.json +53 -19
- package/scripts/consumer-template-ci.mjs +157 -0
- package/scripts/coverage-all.mjs +163 -0
- package/scripts/dashboard.mjs +89 -0
- package/scripts/hash-password.mjs +1 -1
- package/scripts/lib/coverage-model.mjs +51 -0
- package/scripts/port-graph.mjs +350 -0
- package/scripts/typecheck.mjs +11 -0
- package/templates/starter/app/blocks/Prose.vue +1 -1
- package/layers/access/server/utils/csrf.ts +0 -39
- package/layers/access/server/utils/grant-registry.ts +0 -36
- package/layers/access/server/utils/guard.ts +0 -44
- package/layers/access/server/utils/ip-allowlist.ts +0 -79
- package/layers/access/server/utils/policy.ts +0 -98
- package/layers/access/server/utils/public-resources.ts +0 -11
- package/layers/access/server/utils/render-context.ts +0 -41
- package/layers/access/server/utils/require-admin.ts +0 -15
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
- package/layers/auth/server/api/auth/login.post.ts +0 -38
- package/layers/auth/server/api/auth/logout.post.ts +0 -9
- package/layers/auth/server/api/auth/session.get.ts +0 -1
- package/layers/auth/server/utils/admin-credential.ts +0 -42
- package/layers/auth/server/utils/client-ip.ts +0 -88
- package/layers/auth/server/utils/login-throttle.ts +0 -126
- package/layers/auth/server/utils/password.ts +0 -52
- package/layers/auth/server/utils/session-cookie.ts +0 -44
- package/layers/auth/server/utils/session-epoch.ts +0 -96
- package/layers/auth/server/utils/session.ts +0 -88
- package/layers/collections/server/collections/pages.ts +0 -19
- package/layers/collections/server/utils/populate-relations.ts +0 -79
- package/layers/core/app/utils/bulk-actions.ts +0 -9
- package/layers/core/app/utils/filter-ops.ts +0 -98
- package/layers/core/app/utils/list-limits.ts +0 -27
- package/layers/core/app/utils/locale-path.ts +0 -12
- package/layers/core/app/utils/slugify.ts +0 -16
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
- package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
- package/layers/core/server/api/[collection]/[id].get.ts +0 -11
- package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
- package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
- package/layers/core/server/api/[collection]/index.get.ts +0 -24
- package/layers/core/server/api/[collection]/index.post.ts +0 -8
- package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
- package/layers/core/server/api/[collection]/index.put.ts +0 -24
- package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
- package/layers/core/server/api/[collection]/options.get.ts +0 -18
- package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
- package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
- package/layers/core/server/api/[collection]/translations.get.ts +0 -25
- package/layers/core/server/api/collections/[name].get.ts +0 -4
- package/layers/core/server/api/collections/index.get.ts +0 -3
- package/layers/core/server/api/references/broken.get.ts +0 -9
- package/layers/core/server/api/references/referrers.get.ts +0 -24
- package/layers/core/server/database/record-refs.ts +0 -20
- package/layers/core/server/plugins/03.record-refs.ts +0 -12
- package/layers/core/server/schema/bootstrap.ts +0 -41
- package/layers/core/server/schema/desired.ts +0 -99
- package/layers/core/server/schema/dev-sync.ts +0 -23
- package/layers/core/server/schema/dialect.ts +0 -61
- package/layers/core/server/schema/diff.ts +0 -110
- package/layers/core/server/schema/introspect.ts +0 -68
- package/layers/core/server/schema/model.ts +0 -61
- package/layers/core/server/schema/render-sqlite.ts +0 -89
- package/layers/core/server/schema/sync.ts +0 -201
- package/layers/core/server/utils/blocks.ts +0 -6
- package/layers/core/server/utils/collection-types.ts +0 -28
- package/layers/core/server/utils/crud.ts +0 -578
- package/layers/core/server/utils/db.ts +0 -26
- package/layers/core/server/utils/defineCollection.ts +0 -213
- package/layers/core/server/utils/duplicate.ts +0 -162
- package/layers/core/server/utils/field-layout.ts +0 -125
- package/layers/core/server/utils/filter-predicate.ts +0 -101
- package/layers/core/server/utils/http.ts +0 -50
- package/layers/core/server/utils/if-unmodified.ts +0 -13
- package/layers/core/server/utils/kestrel-config.ts +0 -451
- package/layers/core/server/utils/key-lock.ts +0 -33
- package/layers/core/server/utils/locale.ts +0 -53
- package/layers/core/server/utils/page-route.ts +0 -58
- package/layers/core/server/utils/page-slug.ts +0 -127
- package/layers/core/server/utils/picker.ts +0 -82
- package/layers/core/server/utils/populate.ts +0 -50
- package/layers/core/server/utils/prerender-memo.ts +0 -23
- package/layers/core/server/utils/read-capture.ts +0 -37
- package/layers/core/server/utils/record-ref-index.ts +0 -200
- package/layers/core/server/utils/registry.ts +0 -24
- package/layers/core/server/utils/resolve-scope.ts +0 -79
- package/layers/core/server/utils/seo.ts +0 -42
- package/layers/core/server/utils/serialize-collection.ts +0 -147
- package/layers/core/server/utils/sql.ts +0 -2
- package/layers/core/server/utils/storage.local.ts +0 -117
- package/layers/core/server/utils/storage.s3.ts +0 -197
- package/layers/core/server/utils/storage.ts +0 -90
- package/layers/core/server/utils/write-effects.ts +0 -40
- package/layers/core/server/utils/write-events.ts +0 -31
- package/layers/fields/app/utils/condition.ts +0 -71
- package/layers/fields/app/utils/define-block.ts +0 -13
- package/layers/fields/app/utils/field-constraints.ts +0 -17
- package/layers/fields/app/utils/field-meta.ts +0 -33
- package/layers/fields/app/utils/field-validate.ts +0 -75
- package/layers/fields/app/utils/field-value.ts +0 -7
- package/layers/fields/app/utils/richtext-links.ts +0 -48
- package/layers/fields/server/api/blocks/index.get.ts +0 -9
- package/layers/fields/server/field-registry/index.ts +0 -249
- package/layers/fields/server/field-registry/naming.ts +0 -10
- package/layers/fields/server/field-registry/sanitize.ts +0 -43
- package/layers/fields/server/field-registry/types.ts +0 -19
- package/layers/fields/server/utils/block-ids.ts +0 -43
- package/layers/fields/server/utils/block-populate.ts +0 -41
- package/layers/fields/server/utils/buildCollection.ts +0 -90
- package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
- package/layers/fields/server/utils/buildTable.ts +0 -113
- package/layers/fields/server/utils/conditional-required.ts +0 -25
- package/layers/fields/server/utils/defineBlock.ts +0 -86
- package/layers/fields/server/utils/defineFieldType.ts +0 -33
- package/layers/fields/server/utils/extract-refs.ts +0 -152
- package/layers/fields/server/utils/field-populate.ts +0 -68
- package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
- package/layers/media/server/api/media/[id].delete.ts +0 -18
- package/layers/media/server/api/media/[id].get.ts +0 -10
- package/layers/media/server/api/media/[id].patch.test.ts +0 -66
- package/layers/media/server/api/media/[id].patch.ts +0 -65
- package/layers/media/server/api/media/backfill.post.ts +0 -18
- package/layers/media/server/api/media/copy.post.ts +0 -7
- package/layers/media/server/api/media/delete.post.ts +0 -13
- package/layers/media/server/api/media/folders.post.ts +0 -18
- package/layers/media/server/api/media/index.get.ts +0 -15
- package/layers/media/server/api/media/index.post.ts +0 -173
- package/layers/media/server/api/media/library.get.ts +0 -17
- package/layers/media/server/api/media/move.post.ts +0 -7
- package/layers/media/server/api/media/rename.post.ts +0 -7
- package/layers/media/server/api/media/resolve.get.ts +0 -13
- package/layers/media/server/collections/media-settings.ts +0 -25
- package/layers/media/server/collections/media.ts +0 -43
- package/layers/media/server/database/folders.ts +0 -10
- package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
- package/layers/media/server/schema-tables/folders.ts +0 -4
- package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
- package/layers/media/server/utils/ai-signal-detect.ts +0 -155
- package/layers/media/server/utils/backfill.ts +0 -144
- package/layers/media/server/utils/derive.ts +0 -59
- package/layers/media/server/utils/folder-paths.ts +0 -42
- package/layers/media/server/utils/folders.ts +0 -12
- package/layers/media/server/utils/library.ts +0 -91
- package/layers/media/server/utils/media-cleanup.ts +0 -27
- package/layers/media/server/utils/media-enabled.ts +0 -23
- package/layers/media/server/utils/media-ops.ts +0 -118
- package/layers/media/server/utils/media-write.ts +0 -13
- package/layers/media/server/utils/naming.ts +0 -52
- package/layers/media/server/utils/ondemand.ts +0 -69
- package/layers/media/server/utils/persist-upload.ts +0 -62
- package/layers/media/server/utils/populate.ts +0 -47
- package/layers/media/server/utils/record.ts +0 -71
- package/layers/media/server/utils/relocate-ops.ts +0 -341
- package/layers/media/server/utils/relocate-plan.ts +0 -63
- package/layers/media/server/utils/resolve.ts +0 -98
- package/layers/media/server/utils/sanitize-svg.ts +0 -12
- package/layers/media/server/utils/sniff.ts +0 -68
- package/layers/media/server/utils/storage-relocate.ts +0 -139
- package/layers/media/server/utils/translations.ts +0 -19
- package/layers/media/server/utils/usages.ts +0 -66
- package/layers/media/server/utils/variant-capture.ts +0 -70
- package/layers/media/server/utils/variants.ts +0 -123
- package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
- package/layers/public/server/api/links/resolve.get.ts +0 -19
- package/layers/public/server/api/preview.get.ts +0 -28
- package/layers/public/server/api/preview.post.ts +0 -93
- package/layers/public/server/api/publish-status.get.ts +0 -58
- package/layers/public/server/api/publish.post.ts +0 -84
- package/layers/public/server/api/route.get.ts +0 -54
- package/layers/public/server/collections/redirects.ts +0 -75
- package/layers/public/server/collections/site.ts +0 -44
- package/layers/public/server/database/publish-deps.ts +0 -12
- package/layers/public/server/database/publish-status.ts +0 -19
- package/layers/public/server/schema-tables/publish-deps.ts +0 -4
- package/layers/public/server/schema-tables/publish-status.ts +0 -4
- package/layers/public/server/utils/link-resolve.ts +0 -38
- package/layers/public/server/utils/llms-full.ts +0 -125
- package/layers/public/server/utils/llms.ts +0 -52
- package/layers/public/server/utils/page-resolve.ts +0 -208
- package/layers/public/server/utils/populate-links.ts +0 -66
- package/layers/public/server/utils/preview-token.ts +0 -109
- package/layers/public/server/utils/publish/coalesce.ts +0 -22
- package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
- package/layers/public/server/utils/publish/deps.ts +0 -87
- package/layers/public/server/utils/publish/invalidation.ts +0 -225
- package/layers/public/server/utils/publish/pending.ts +0 -74
- package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
- package/layers/public/server/utils/publish/publish-status.ts +0 -72
- package/layers/public/server/utils/publish/publisher.ts +0 -362
- package/layers/public/server/utils/publish/queue.ts +0 -67
- package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
- package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
- package/layers/public/server/utils/publish/route-for-record.ts +0 -16
- package/layers/public/server/utils/publish/route-keys.ts +0 -13
- package/layers/public/server/utils/richtext-markdown.ts +0 -260
- package/layers/public/server/utils/site-url.ts +0 -40
- package/layers/public/server/utils/sitemap.ts +0 -99
|
@@ -70,9 +70,86 @@ export function collectBlockSfcs(layerRoots: string[], subdir = 'app/blocks', li
|
|
|
70
70
|
return [...byName.values()].sort()
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Each layer's OWN `server/db/manifest.ts` (ADR-0012 ownership manifest), one per layer that has one.
|
|
75
|
+
* Unlike `collectDefinitions`, this does NOT dedupe by basename: every layer's manifest is a distinct
|
|
76
|
+
* module contribution (no shadow/override semantics — `media`'s manifest and `public`'s manifest both
|
|
77
|
+
* happen to be named `manifest.ts`, and both must survive). Layer order is whatever `layerRoots` is.
|
|
78
|
+
*/
|
|
79
|
+
export function collectManifestFiles(layerRoots: string[], list?: Lister): string[] {
|
|
80
|
+
const files: string[] = []
|
|
81
|
+
for (const root of layerRoots) {
|
|
82
|
+
for (const file of listDefinitionFiles(join(root, 'server/db'), list)) {
|
|
83
|
+
if (basename(file) === 'manifest.ts') files.push(file)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return files
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
export function renderRegistry(absPaths: string[]): string {
|
|
74
90
|
if (!absPaths.length) return 'export default []'
|
|
75
91
|
const imports = absPaths.map((p, i) => `import _${i} from ${JSON.stringify(p)}`)
|
|
76
92
|
const arr = absPaths.map((_, i) => `_${i}`).join(', ')
|
|
77
93
|
return `${imports.join('\n')}\nexport default [${arr}]`
|
|
78
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A registry combining package-provided items (each package's `kestrelDiscovery.<property>`, read via a
|
|
98
|
+
* bare specifier — no filesystem/`node_modules` guessing) with consumer/layer-scanned files, with a
|
|
99
|
+
* SAME-NAME layer item overriding a package's (via `mergeKestrelDiscovered`, evaluated at virtual-load
|
|
100
|
+
* time — package names aren't knowable at codegen time without loading the package, which this
|
|
101
|
+
* deliberately avoids doing at build time). `nameOfExpr` is a JS arrow-function source string the
|
|
102
|
+
* generated module evaluates per item (e.g. `'(x) => x.name'` for collections,
|
|
103
|
+
* `'(x) => __kestrelTableName(x)'` for schema-tables, paired with `extraImports` supplying
|
|
104
|
+
* `__kestrelTableName`).
|
|
105
|
+
*/
|
|
106
|
+
export function renderPackageMergedRegistry(opts: {
|
|
107
|
+
packages: string[]
|
|
108
|
+
property: 'collections' | 'schemaTables'
|
|
109
|
+
consumerFiles: string[]
|
|
110
|
+
nameOfExpr: string
|
|
111
|
+
extraImports?: string
|
|
112
|
+
preamble?: string
|
|
113
|
+
}): string {
|
|
114
|
+
const pkgImports = opts.packages.map((spec, i) => `import { kestrelDiscovery as __pkg${i} } from ${JSON.stringify(spec)}`)
|
|
115
|
+
const pkgSpread = opts.packages.map((_, i) => `...(__pkg${i}.${opts.property} ?? [])`).join(', ')
|
|
116
|
+
const consumerImports = opts.consumerFiles.map((p, i) => `import _c${i} from ${JSON.stringify(p)}`)
|
|
117
|
+
const consumerArr = opts.consumerFiles.map((_, i) => `_c${i}`).join(', ')
|
|
118
|
+
return [
|
|
119
|
+
opts.preamble ?? '',
|
|
120
|
+
opts.extraImports ?? '',
|
|
121
|
+
`import { mergeKestrelDiscovered } from '@michaelthielemann/kestrel-core'`,
|
|
122
|
+
...pkgImports,
|
|
123
|
+
...consumerImports,
|
|
124
|
+
`export default mergeKestrelDiscovered([${pkgSpread}], [${consumerArr}], ${opts.nameOfExpr})`,
|
|
125
|
+
].filter(Boolean).join('\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A registry concatenating package-provided items (`kestrelDiscovery.<property>`) with consumer/layer-
|
|
130
|
+
* scanned files — NO dedup (mirrors `collectManifestFiles`'s own "every contribution is distinct, no
|
|
131
|
+
* shadow semantics" contract; unlike collections/schema-tables, two manifests sharing a name is not a
|
|
132
|
+
* schema conflict, so there is nothing to override).
|
|
133
|
+
*/
|
|
134
|
+
export function renderPackageConcatRegistry(opts: {
|
|
135
|
+
packages: string[]
|
|
136
|
+
property: 'manifest'
|
|
137
|
+
consumerFiles: string[]
|
|
138
|
+
preamble?: string
|
|
139
|
+
}): string {
|
|
140
|
+
const pkgImports = opts.packages.map((spec, i) => `import { kestrelDiscovery as __pkg${i} } from ${JSON.stringify(spec)}`)
|
|
141
|
+
// `opts.packages` is expected to be pre-filtered to packages that actually contribute this property (the
|
|
142
|
+
// caller's per-category list), so every `.manifest` read here SHOULD be defined — but the spread stays
|
|
143
|
+
// conditional (`...(x ? [x] : [])`, not a bare `x`) as a defense-in-depth guard: a package mis-listed in
|
|
144
|
+
// the wrong `PACKAGE_*` category contributes nothing instead of polluting the array with `undefined`.
|
|
145
|
+
const pkgItems = opts.packages.map((_, i) => `...(__pkg${i}.${opts.property} ? [__pkg${i}.${opts.property}] : [])`)
|
|
146
|
+
const consumerImports = opts.consumerFiles.map((p, i) => `import _c${i} from ${JSON.stringify(p)}`)
|
|
147
|
+
const consumerArr = opts.consumerFiles.map((_, i) => `_c${i}`)
|
|
148
|
+
const all = [...pkgItems, ...consumerArr].join(', ')
|
|
149
|
+
return [
|
|
150
|
+
opts.preamble ?? '',
|
|
151
|
+
...pkgImports,
|
|
152
|
+
...consumerImports,
|
|
153
|
+
`export default [${all}]`,
|
|
154
|
+
].filter(Boolean).join('\n')
|
|
155
|
+
}
|
|
@@ -10,6 +10,11 @@ declare module '#kestrel/schema-tables' {
|
|
|
10
10
|
const tables: unknown[]
|
|
11
11
|
export default tables
|
|
12
12
|
}
|
|
13
|
+
declare module '#kestrel/module-manifests' {
|
|
14
|
+
import type { OwnershipManifest } from '@michaelthielemann/kestrel-contracts'
|
|
15
|
+
const manifests: OwnershipManifest[]
|
|
16
|
+
export default manifests
|
|
17
|
+
}
|
|
13
18
|
declare module '#build/kestrel-layouts.mjs' {
|
|
14
19
|
export const kestrelLayouts: string[]
|
|
15
20
|
}
|
|
@@ -5,6 +5,30 @@ import { KESTREL_COMPONENT_PREFIX, KESTREL_OVERRIDE_PRIORITY } from './shared'
|
|
|
5
5
|
/** Where a consumer places a component that replaces a shipped one, relative to their `srcDir`. */
|
|
6
6
|
export const KESTREL_OVERRIDE_DIR = 'Kestrel/components'
|
|
7
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
|
+
|
|
8
32
|
export default defineNuxtModule({
|
|
9
33
|
meta: { name: 'kestrel-component-namespace' },
|
|
10
34
|
setup(_options, nuxt) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { defineNuxtModule } from '@nuxt/kit'
|
|
3
|
-
import { resolveKestrel, type KestrelConfig } from '
|
|
3
|
+
import { resolveKestrel, type KestrelConfig } from '@michaelthielemann/kestrel-core'
|
|
4
4
|
import { diagnoseAppShell } from './app-shell'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -78,6 +78,11 @@ export default defineNuxtModule<KestrelConfig>({
|
|
|
78
78
|
srv.aiDisclosure = c.aiDisclosure
|
|
79
79
|
// Answer-engine toggles; `llmsFull` is server-only (it gates a Nitro route and what the publisher writes).
|
|
80
80
|
srv.seo = c.seo
|
|
81
|
+
// Read by delivery-live's catch-all/read pipeline (see `KestrelConfig.delivery`'s own TSDoc) to
|
|
82
|
+
// decide whether to answer at all — everything else about it stays 'static'-shaped either way.
|
|
83
|
+
srv.delivery = c.delivery
|
|
84
|
+
// Extra path prefixes the live catch-all must fall through on (see `KestrelConfig.deliveryExempt`).
|
|
85
|
+
srv.deliveryExempt = c.deliveryExempt
|
|
81
86
|
// Static-publish target for the runtime publisher (server-only). S3 creds are env-only; prefer the
|
|
82
87
|
// output-specific creds, fall back to the shared media creds so a single S3 account "just works".
|
|
83
88
|
srv.output = {
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const PLUGIN_EXTENSIONS = ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts', 'tsx', 'jsx']
|
|
5
|
+
|
|
6
|
+
/** One of Kestrel's own `server/plugins/**` files, in the position it must execute at. */
|
|
7
|
+
export interface PluginEntry {
|
|
8
|
+
/** The layer directory name (`layers/<layer>`). */
|
|
9
|
+
layer: string
|
|
10
|
+
/** Path relative to the layer root. */
|
|
11
|
+
file: string
|
|
12
|
+
/** Keys (`${layer}/${file}`) of OTHER entries that MUST appear at a lower index than this one — empty
|
|
13
|
+
* when nothing's correctness depends on this entry's position. Machine-checked by `validatePluginOrder`
|
|
14
|
+
* against `PLUGIN_ORDER`'s actual array positions, not merely asserted. */
|
|
15
|
+
after: string[]
|
|
16
|
+
/** One line: the real dependency this entry's `after` encodes (when non-empty), or the evidence it's
|
|
17
|
+
* safe to reorder (when empty) — always named, never assumed. */
|
|
18
|
+
evidence: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `${layer}/${file}` — the identity `after` references and `validatePluginOrder` indexes by. */
|
|
22
|
+
const key = (p: Pick<PluginEntry, 'layer' | 'file'>): string => `${p.layer}/${p.file}`
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The declared execution order for every plugin Kestrel itself ships — DATA, read and pushed by
|
|
26
|
+
* `layers/core/modules/plugin-order/index.ts`, replacing Nitro's own layer-then-filename sort for these
|
|
27
|
+
* files. `validatePluginOrder` (below) makes TWO kinds of drift a loud build failure instead of a silent
|
|
28
|
+
* Nitro auto-append or an unenforced comment: (1) a plugin file on disk that isn't declared here (or a
|
|
29
|
+
* declared entry with no file behind it), and (2) an `after` dependency whose real position in this array
|
|
30
|
+
* doesn't actually satisfy it — a reshuffle that moves a dependent entry ahead of what it depends on fails
|
|
31
|
+
* the SAME way a missing/phantom file does, not silently.
|
|
32
|
+
*
|
|
33
|
+
* Audited against a real boot in both contexts (in-repo `nuxt build` and the playground's
|
|
34
|
+
* `extends: ['..', ...]` composition) via a throwaway probe module reading the resolved
|
|
35
|
+
* `nitro.options.plugins`. Confirmed identical for Kestrel's own 6 layers in both contexts:
|
|
36
|
+
* core → fields → media → auth → collections → public,
|
|
37
|
+
* filename-sort within each layer — which is exactly what this list encodes explicitly.
|
|
38
|
+
*/
|
|
39
|
+
export const PLUGIN_ORDER: PluginEntry[] = [
|
|
40
|
+
{
|
|
41
|
+
layer: 'core', file: 'server/plugins/00.config.ts', after: [],
|
|
42
|
+
evidence: 'Nothing depends on core/00.config.ts to come after anything else; everything downstream depends on IT (see the after: entries below).',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
layer: 'core', file: 'server/plugins/00.migrate.ts', after: ['core/server/plugins/00.config.ts'],
|
|
46
|
+
evidence: 'Calls useDb(), which reads the config provider 00.config.ts seeds.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
layer: 'core', file: 'server/plugins/01.register-introspection-pipeline.ts', after: [],
|
|
50
|
+
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
layer: 'core', file: 'server/plugins/01.register-openapi-pipeline.ts', after: [],
|
|
54
|
+
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
layer: 'core', file: 'server/plugins/01.register-tooling-pipelines.ts', after: [],
|
|
58
|
+
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
layer: 'core', file: 'server/plugins/02.schema-sync.ts', after: ['core/server/plugins/00.migrate.ts'],
|
|
62
|
+
evidence: 'Dev-only additive auto-migration; its own header comment: "Runs after 00.migrate, so built-in collections are already migrated and this is a no-op on them".',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
layer: 'core', file: 'server/plugins/04.outbox-worker.ts', after: [],
|
|
66
|
+
evidence: 'Starts its own setInterval poll loop against useDb(); no other plugin\'s behavior depends on when this one starts polling (the poll itself is unconditional, no readiness handshake with another plugin).',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
layer: 'core', file: 'server/plugins/05.reindex-refs.ts', after: [],
|
|
70
|
+
evidence: 'registerReindexRefs() registers a handler on the outbox event bus; dispatched later by the outbox worker, never read at another plugin\'s init.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
layer: 'fields', file: 'server/plugins/01.register-blocks-pipeline.ts', after: [],
|
|
74
|
+
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
layer: 'fields', file: 'server/plugins/01.register-field-populate.ts', after: [],
|
|
78
|
+
evidence: 'Registers the single global row populator; field populators (media/link/richtext/relation) are looked up at READ time, so this plugin\'s order vs. the layer plugins that register them does not matter (stated in the plugin\'s own header comment).',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
layer: 'media', file: 'server/plugins/01.register-media-pipelines.ts', after: [],
|
|
82
|
+
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
layer: 'media', file: 'server/plugins/02.register-media.ts', after: [],
|
|
86
|
+
evidence: 'registerFieldPopulator; looked up at read time, not at another plugin\'s init.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
layer: 'media', file: 'server/plugins/04.variant-capture.ts', after: [],
|
|
90
|
+
evidence: 'Hooks the render request context (isRendererContext) and stashes discovered variants during SSR; reconciled per-render, no dependency on another plugin having run first.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
layer: 'media', file: 'server/plugins/05.media-cleanup.ts', after: [],
|
|
94
|
+
evidence: 'registerMediaCleanup() registers a handler on the outbox event bus; dispatched later.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
layer: 'auth', file: 'server/plugins/01.register-auth-pipelines.ts', after: [],
|
|
98
|
+
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
layer: 'collections', file: 'server/plugins/01.register.ts', after: [],
|
|
102
|
+
evidence: 'Reads the #kestrel/collections and #kestrel/blocks VIRTUALS directly (never allCollections()); writes the runtime registry, but no plugin in this list reads that registry at its own init — only at request time, well after every plugin has finished (documented invariant, docs/internals/architecture.md § Server plugins).',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
layer: 'collections', file: 'server/plugins/02.register-relation-populate.ts', after: [],
|
|
106
|
+
evidence: 'registerFieldPopulator; looked up at read time.',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
layer: 'public', file: 'server/plugins/00.ensure-snapshot-triggers.ts',
|
|
110
|
+
after: ['core/server/plugins/00.migrate.ts', 'core/server/plugins/02.schema-sync.ts'],
|
|
111
|
+
evidence: 'THE one real cross-layer dependency: needs the snapshot store\'s table to already exist, created by core\'s 00.migrate (in-repo) or 02.schema-sync (consumer). Self-tolerant even so — a boot before either has run logs a warning and returns rather than crashing (ensureSnapshotTriggers\'s own TSDoc); still placed after both so the warning path is never hit in practice.',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
layer: 'public', file: 'server/plugins/01.register-public-pipelines.ts', after: [],
|
|
115
|
+
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
layer: 'public', file: 'server/plugins/02.register-links.ts', after: [],
|
|
119
|
+
evidence: 'registerFieldPopulator; looked up at read time.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
layer: 'public', file: 'server/plugins/03.redirects.ts', after: [],
|
|
123
|
+
evidence: 'registerAfterStep registers a DEFERRED, when-scoped pipeline hook; invoked on a future write, not at another plugin\'s init.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
layer: 'public', file: 'server/plugins/05.plan-publish.ts', after: [],
|
|
127
|
+
evidence: 'Registers planPublish on the outbox bus; dispatched later by the outbox worker, after commit — never at plugin init.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
layer: 'public', file: 'server/plugins/zz.publish.ts', after: [],
|
|
131
|
+
evidence: 'Registers a deferred CRITICAL after-step and starts the boot publish; the boot publish\'s own registry read sits after an `await` inside publishFull, so it resumes only once the synchronous plugin loop (including collections\' 01.register) has already finished — order-free by construction (documented invariant, docs/internals/architecture.md § Server plugins).',
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
/** POSIX-normalize a path for comparison — `path.join`'s OUTPUT is OS-native (backslash on Windows), but
|
|
136
|
+
* `PluginEntry.file` is authored with literal forward slashes (`'server/plugins/00.config.ts'`) and Nuxt's
|
|
137
|
+
* own `_layers[].cwd` values are not guaranteed to be OS-native either (bundlers commonly normalize to
|
|
138
|
+
* POSIX internally regardless of platform). Comparing through one canonical POSIX form — rather than two
|
|
139
|
+
* parallel branches guessing which separator a given string happens to use — removes the asymmetry a
|
|
140
|
+
* Windows build would otherwise hit. */
|
|
141
|
+
const toPosix = (p: string): string => p.split(sep).join('/')
|
|
142
|
+
|
|
143
|
+
/** Find layer `name`'s root among Nuxt's resolved `_layers` cwds. Nitro plugin paths must be absolute
|
|
144
|
+
* filesystem paths — there is no bare-specifier equivalent for a Nitro plugin the way there is for a
|
|
145
|
+
* package export — so this lookup (mirroring the pattern auto-discovery used for the pre-package fields
|
|
146
|
+
* seed) is still needed. */
|
|
147
|
+
function layerRoot(roots: string[], name: string): string {
|
|
148
|
+
const root = roots.find((r) => toPosix(r).endsWith(`/layers/${name}`))
|
|
149
|
+
if (!root) throw new Error(`[kestrel] plugin-order: could not locate the "${name}" layer root`)
|
|
150
|
+
return root
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `PLUGIN_ORDER` (or an injected `list`, for testing drift scenarios without editing the real constant),
|
|
154
|
+
* resolved to absolute file paths in declared order — what gets pushed into `nitro.options.plugins`. */
|
|
155
|
+
export function resolvePluginOrder(roots: string[], list: PluginEntry[] = PLUGIN_ORDER): string[] {
|
|
156
|
+
return list.map((p) => join(layerRoot(roots, p.layer), p.file))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Nitro's OWN plugin file extension set (`nitropack`'s `GLOB_SCAN_PATTERN` for `plugins/`) — matched
|
|
160
|
+
* exactly, not approximated, so this scan can never be fooled by a file Nitro would pick up that this
|
|
161
|
+
* didn't look for. */
|
|
162
|
+
const PLUGIN_FILE_RE = new RegExp(`\\.(${PLUGIN_EXTENSIONS.join('|')})$`)
|
|
163
|
+
const isPluginFile = (name: string) => PLUGIN_FILE_RE.test(name) && !name.endsWith('.test.ts') && !/\.test\.(js|mjs|cjs|mts|cts|tsx|jsx)$/.test(name)
|
|
164
|
+
|
|
165
|
+
/** Every file under `dir` Nitro's own scan would find — RECURSIVELY (Nitro's glob is `plugins/**\/*.{ext}`,
|
|
166
|
+
* not a flat listing), matching Nitro's exact extension set. Mirrors Nitro's own convention for real, not
|
|
167
|
+
* approximately. */
|
|
168
|
+
function walkPluginFiles(dir: string, out: string[] = []): string[] {
|
|
169
|
+
if (!existsSync(dir)) return out
|
|
170
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
171
|
+
const full = join(dir, entry.name)
|
|
172
|
+
if (entry.isDirectory()) walkPluginFiles(full, out)
|
|
173
|
+
else if (isPluginFile(entry.name)) out.push(full)
|
|
174
|
+
}
|
|
175
|
+
return out
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Every plugin file Nitro would ACTUALLY scan across the layers `list` declares — derived from `list`
|
|
179
|
+
* itself (not a separately hardcoded layer set) so an injected test `list` governs exactly the layers it
|
|
180
|
+
* claims to, never silently falling back to a hardcoded layer set regardless of what was actually passed
|
|
181
|
+
* in — the shape a test injecting a fully isolated fake `list`/`roots` pair depends on. */
|
|
182
|
+
function scanActualPlugins(roots: string[], list: PluginEntry[]): string[] {
|
|
183
|
+
const layers = [...new Set(list.map((p) => p.layer))]
|
|
184
|
+
const found: string[] = []
|
|
185
|
+
for (const layer of layers) found.push(...walkPluginFiles(join(layerRoot(roots, layer), 'server/plugins')))
|
|
186
|
+
return found.sort()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** `PLUGIN_ORDER` entries reachable from `list`, by `key`. */
|
|
190
|
+
function assertOrderConstraintsSatisfied(list: PluginEntry[]): void {
|
|
191
|
+
const indexOf = new Map(list.map((p, i) => [key(p), i]))
|
|
192
|
+
const violations: string[] = []
|
|
193
|
+
for (const p of list) {
|
|
194
|
+
for (const dep of p.after) {
|
|
195
|
+
const depIndex = indexOf.get(dep)
|
|
196
|
+
const ownIndex = indexOf.get(key(p))!
|
|
197
|
+
if (depIndex === undefined) {
|
|
198
|
+
violations.push(`${key(p)} declares after: "${dep}", but no such entry exists in PLUGIN_ORDER`)
|
|
199
|
+
} else if (depIndex >= ownIndex) {
|
|
200
|
+
violations.push(`${key(p)} must come after ${dep} (${p.evidence}), but is declared at index ${ownIndex} vs. ${dep}'s index ${depIndex}`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (violations.length) {
|
|
205
|
+
throw new Error(['[kestrel] plugin-order: PLUGIN_ORDER violates a declared after: dependency.', ...violations.map((v) => ` ${v}`)].join('\n'))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* LOUD failure for BOTH kinds of drift: (1) a plugin file present on disk but missing from `PLUGIN_ORDER`
|
|
211
|
+
* (which Nitro's own scan-and-append would otherwise silently tack onto the END, in undeclared position —
|
|
212
|
+
* see nitropack's `scanPlugins`/`options.plugins.push` for exactly that fallback behavior) or a declared
|
|
213
|
+
* entry whose file no longer exists; (2) a declared `after` dependency the array's ACTUAL positions don't
|
|
214
|
+
* satisfy — a reshuffle, not just a deletion, fails just as loudly. Throws — the ONLY way a Nuxt module's
|
|
215
|
+
* `setup()` can fail the build loudly (a Nitro hook can't itself abort configuration once already running).
|
|
216
|
+
* Called from `setup()`, before the `nitro:config` hook — a build-time failure, not a Nitro-time one.
|
|
217
|
+
*/
|
|
218
|
+
export function validatePluginOrder(roots: string[], list: PluginEntry[] = PLUGIN_ORDER): void {
|
|
219
|
+
assertOrderConstraintsSatisfied(list)
|
|
220
|
+
|
|
221
|
+
const declared = new Set(resolvePluginOrder(roots, list))
|
|
222
|
+
const actual = new Set(scanActualPlugins(roots, list))
|
|
223
|
+
const missing = [...actual].filter((f) => !declared.has(f)) // on disk, not declared
|
|
224
|
+
const phantom = [...declared].filter((f) => !actual.has(f)) // declared, not on disk
|
|
225
|
+
if (missing.length === 0 && phantom.length === 0) return
|
|
226
|
+
const lines: string[] = ['[kestrel] plugin-order: PLUGIN_ORDER has drifted from the real server/plugins/ files.']
|
|
227
|
+
if (missing.length) lines.push(` On disk but NOT declared (would silently land last, undeclared, via Nitro's own scan-and-append): ${missing.join(', ')}`)
|
|
228
|
+
if (phantom.length) lines.push(` Declared but the file does not exist: ${phantom.join(', ')}`)
|
|
229
|
+
lines.push(' Fix PLUGIN_ORDER in layers/core/modules/plugin-order/plugin-order.ts.')
|
|
230
|
+
throw new Error(lines.join('\n'))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The permanent, in-process observer of the REAL merge: `validatePluginOrder`/`resolvePluginOrder` only
|
|
235
|
+
* prove what THIS module pushes — they cannot see what Nitro's own scan-and-append, another module's
|
|
236
|
+
* `nitro:config` hook, or Nitro's own internal/dev-tooling plugins do to the array afterward. Called from
|
|
237
|
+
* `index.ts`'s `nitro:init` hook (which receives the fully-built Nitro instance, after every `nitro:config`
|
|
238
|
+
* hook AND Nitro's own scan have both already run) against the REAL `nitro.options.plugins`, this asserts
|
|
239
|
+
* our declared order survived that whole pipeline intact: every `expected` path appears, in order, as ONE
|
|
240
|
+
* CONTIGUOUS RUN somewhere in the real array — NOT necessarily a prefix. A real `nuxt dev` boot pushes
|
|
241
|
+
* `@nuxt/devtools`'/`@nuxt/nitro-server`'s own internal plugins (an inline devtools runtime, dev-server
|
|
242
|
+
* request logging) ahead of everything via their own earlier-registered `nitro:config` hooks — legitimate
|
|
243
|
+
* Nuxt/Nitro tooling this module neither owns nor should reject, so "prefix" is the wrong bar; "our own
|
|
244
|
+
* declared plugins never get split apart or interleaved with anyone else's" is the real, checkable
|
|
245
|
+
* invariant, and is what this asserts. Also asserts the real array has no duplicate entries anywhere
|
|
246
|
+
* (Nitro's own `!plugins.includes(x)` scan guard is SUPPOSED to prevent one, but this checks the outcome
|
|
247
|
+
* directly rather than trusting that guard blind). Throws — a boot-time, not merely a config-time, failure.
|
|
248
|
+
*/
|
|
249
|
+
export function assertEffectiveOrderMatches(actualPlugins: (string | undefined)[], expected: string[]): void {
|
|
250
|
+
const cleaned = actualPlugins.filter((p): p is string => typeof p === 'string')
|
|
251
|
+
const seen = new Set<string>()
|
|
252
|
+
const duplicates = new Set<string>()
|
|
253
|
+
for (const p of cleaned) {
|
|
254
|
+
if (seen.has(p)) duplicates.add(p)
|
|
255
|
+
seen.add(p)
|
|
256
|
+
}
|
|
257
|
+
if (duplicates.size) {
|
|
258
|
+
throw new Error(`[kestrel] plugin-order: nitro.options.plugins contains duplicate entries: ${[...duplicates].join(', ')}`)
|
|
259
|
+
}
|
|
260
|
+
if (expected.length === 0) return
|
|
261
|
+
let start = -1
|
|
262
|
+
for (let i = 0; i + expected.length <= cleaned.length; i++) {
|
|
263
|
+
if (expected.every((e, j) => cleaned[i + j] === e)) { start = i; break }
|
|
264
|
+
}
|
|
265
|
+
if (start === -1) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
'[kestrel] plugin-order: the REAL nitro.options.plugins does not contain PLUGIN_ORDER as one unbroken, '
|
|
268
|
+
+ "in-order run — something split our declared block apart or interleaved into it.\n"
|
|
269
|
+
+ ` Expected (contiguous, any position): ${expected.join(', ')}\n`
|
|
270
|
+
+ ` Actual full list: ${cleaned.join(', ')}`,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -5,5 +5,6 @@ export default defineNuxtConfig({
|
|
|
5
5
|
fileURLToPath(new URL('./modules/component-namespace/index.ts', import.meta.url)),
|
|
6
6
|
fileURLToPath(new URL('./modules/auto-discovery/index.ts', import.meta.url)),
|
|
7
7
|
fileURLToPath(new URL('./modules/kestrel/index.ts', import.meta.url)),
|
|
8
|
+
fileURLToPath(new URL('./modules/plugin-order/index.ts', import.meta.url)),
|
|
8
9
|
],
|
|
9
10
|
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getRequestHeader } from 'h3'
|
|
2
|
+
import {
|
|
3
|
+
MAX_JSON_BODY, assertBodyLimit, isRoutablePipeline, toHttpError, tryResolveDefaultPipeline,
|
|
4
|
+
parseFilter, parsePipelineRoute, getCollectionOr404, useDb, readIfUnmodifiedSince,
|
|
5
|
+
} from '@michaelthielemann/kestrel-core'
|
|
6
|
+
import type { PipelineContext, PipelineTrace } from '@michaelthielemann/kestrel-core'
|
|
7
|
+
import { runPipelineForEventAuto, resolveEventPrincipal } from '@michaelthielemann/kestrel-access'
|
|
8
|
+
// The one handler behind the whole pipeline URL scheme:
|
|
9
|
+
//
|
|
10
|
+
// GET /api/<collection>/<readPipeline>[/<id>]
|
|
11
|
+
// POST /api/<collection>/<writePipeline>[/<id>]
|
|
12
|
+
// POST /api/<pipeline> (collection-less: login, publish, createPreview)
|
|
13
|
+
//
|
|
14
|
+
// It is the ONLY route file under server/api: every endpoint is a pipeline, and this is where each one is
|
|
15
|
+
// resolved, verb-checked and run.
|
|
16
|
+
|
|
17
|
+
/** Reads take their whole input from the query string, in the shape the read steps expect. One builder for
|
|
18
|
+
* every read pipeline — the generic list/detail reads and the editor's tooling reads pick the keys they
|
|
19
|
+
* need out of the same object. */
|
|
20
|
+
function readInput(query: Record<string, unknown>): Record<string, unknown> {
|
|
21
|
+
return {
|
|
22
|
+
...query,
|
|
23
|
+
locale: query.locale as string | undefined,
|
|
24
|
+
sort: query.sort as string | undefined,
|
|
25
|
+
page: query.page ? Number(query.page) : undefined,
|
|
26
|
+
perPage: query.perPage ? Number(query.perPage) : undefined,
|
|
27
|
+
filter: parseFilter(query),
|
|
28
|
+
depth: query.depth ? Number(query.depth) : 0,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertMethod(method: string, read: boolean, pipeline: string): void {
|
|
33
|
+
const allowed = read ? method === 'GET' || method === 'HEAD' : method === 'POST'
|
|
34
|
+
if (!allowed) {
|
|
35
|
+
throw createError({ statusCode: 405, statusMessage: `${pipeline} is a ${read ? 'read' : 'write'} pipeline — use ${read ? 'GET' : 'POST'}` })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Embed the trace as a `$`-prefixed sidecar, matching the `$media`/`$translations` convention on rows. A
|
|
40
|
+
* batch/array result (createMany, duplicate) or a bare value (a not-found singleton's `null`) has no
|
|
41
|
+
* object to spread into, so it is wrapped under `data` instead. */
|
|
42
|
+
function embedTrace(result: unknown, trace: PipelineTrace): unknown {
|
|
43
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result)) return { data: result, $pipeline: trace }
|
|
44
|
+
return { ...(result as Record<string, unknown>), $pipeline: trace }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function logPipelineRun(trace: PipelineTrace): void {
|
|
48
|
+
const steps = trace.steps.map((s) => `${s.name}${s.status === 'ok' ? '' : `:${s.status}`}=${s.ms}ms`).join(' ')
|
|
49
|
+
const pipeline = trace.collection ? `${trace.collection}/${trace.pipeline}` : trace.pipeline
|
|
50
|
+
console.log(`[kestrel] pipeline ${pipeline} ${steps} total=${trace.ms}ms`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default defineEventHandler(async (event) => {
|
|
54
|
+
const route = parsePipelineRoute(event.path)
|
|
55
|
+
const collection = route.collection === null ? null : getCollectionOr404(route.collection)
|
|
56
|
+
const resolved = tryResolveDefaultPipeline(route.collection, route.pipeline)
|
|
57
|
+
if (!isRoutablePipeline(resolved)) {
|
|
58
|
+
throw createError({ statusCode: 404, statusMessage: `Unknown pipeline: ${route.pipeline}` })
|
|
59
|
+
}
|
|
60
|
+
assertMethod(getMethod(event), resolved.read, route.pipeline)
|
|
61
|
+
|
|
62
|
+
const query = getQuery(event) as Record<string, unknown>
|
|
63
|
+
// The body is buffered HERE, before the pipeline's gates run — so it must be bounded, or an anonymous /
|
|
64
|
+
// cross-site / IP-blocked caller could stream an unbounded body into memory before being refused.
|
|
65
|
+
if (!resolved.read && !resolved.rawBody) assertBodyLimit(getRequestHeader(event, 'content-length'), MAX_JSON_BODY)
|
|
66
|
+
let ctx: PipelineContext | undefined
|
|
67
|
+
let result: unknown
|
|
68
|
+
try {
|
|
69
|
+
result = await runPipelineForEventAuto(event, {
|
|
70
|
+
op: route.pipeline,
|
|
71
|
+
collection,
|
|
72
|
+
db: collection ? useDb() : null,
|
|
73
|
+
id: route.id,
|
|
74
|
+
locale: query.locale as string | undefined,
|
|
75
|
+
// A `rawBody` pipeline owns its own body read (multipart, ciphertext, a size-capped stream), so the
|
|
76
|
+
// router must not consume the stream first.
|
|
77
|
+
input: resolved.read ? readInput(query) : resolved.rawBody ? undefined : await readBody(event),
|
|
78
|
+
// Optimistic concurrency: the editor sends the baseline it loaded, and a stale save is refused with
|
|
79
|
+
// 409. Only `updateOne` has a step that reads it.
|
|
80
|
+
work: resolved.read ? {} : { expectedUpdatedAt: readIfUnmodifiedSince(event) },
|
|
81
|
+
onContext: (c) => { ctx = c },
|
|
82
|
+
})
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// The ONE call site that turns a step-raised KestrelError into an HTTP response (see
|
|
85
|
+
// utils/kestrel-error-map.ts); everything else (the runner, the drivers) passes it through untouched.
|
|
86
|
+
// Gate denials (401/403) never reach here as a KestrelError — they're already createError-shaped by
|
|
87
|
+
// the time they leave the runner (see runner.ts's denied/refused) and pass through unchanged.
|
|
88
|
+
throw toHttpError(error)
|
|
89
|
+
} finally {
|
|
90
|
+
if (import.meta.dev && ctx) logPipelineRun(ctx.trace.toJSON())
|
|
91
|
+
}
|
|
92
|
+
if (route.pipeline === 'createOne' || route.pipeline === 'createMany') setResponseStatus(event, 201)
|
|
93
|
+
|
|
94
|
+
if (ctx && query.debug === 'pipeline' && resolveEventPrincipal(event).role === 'admin') {
|
|
95
|
+
return embedTrace(result, ctx.trace.toJSON())
|
|
96
|
+
}
|
|
97
|
+
return result
|
|
98
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { setResolvedKestrelConfig } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { resolveServerKestrelConfig } from '../utils/server-config'
|
|
3
|
+
|
|
4
|
+
// Runs before every other core plugin (00.migrate.ts included — it calls useDb(), which now reads the
|
|
5
|
+
// provider) and before media/auth/collections/public's own plugins (docs/internals/architecture.md § Server
|
|
6
|
+
// plugins: core → media → auth → collections → public). Resolves the config ONCE with the app's own
|
|
7
|
+
// runtimeConfig-preferred, file/env-fallback precedence and pushes it into the config provider — the seam
|
|
8
|
+
// package code (locale.ts, revision-retention.ts, db.ts today; the future package-side movers of the
|
|
9
|
+
// same files) reads from instead of touching `useRuntimeConfig()` directly.
|
|
10
|
+
export default defineNitroPlugin(() => {
|
|
11
|
+
setResolvedKestrelConfig(resolveServerKestrelConfig())
|
|
12
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
|
4
|
+
import { useDb } from '@michaelthielemann/kestrel-core'
|
|
4
5
|
|
|
5
6
|
export default defineNitroPlugin(() => {
|
|
6
7
|
// The committed drizzle-kit migrations only exist in Kestrel's own repo. When Kestrel is consumed as
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildIntrospectionPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildIntrospectionPipelines()) registerPipeline(def)
|
|
5
|
+
})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildOpenApiPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildOpenApiPipelines()) registerPipeline(def)
|
|
5
|
+
})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildToolingPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildToolingPipelines()) registerPipeline(def)
|
|
5
|
+
})
|