@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
|
@@ -20,12 +20,15 @@ interface ListResponse {
|
|
|
20
20
|
total: number
|
|
21
21
|
page: number
|
|
22
22
|
perPage: number
|
|
23
|
+
/** Set by `validateOut` — count of rows in `data` replaced with the quarantine shape. */
|
|
24
|
+
quarantinedCount?: number
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function useListRows(opts: ListRowsOptions) {
|
|
26
28
|
const { t } = useT()
|
|
27
29
|
const rows = ref<Record<string, unknown>[]>([])
|
|
28
30
|
const total = ref(0)
|
|
31
|
+
const quarantinedCount = ref(0)
|
|
29
32
|
// Surfaced when a (re)fetch fails — an inline error + Retry, never a silent stale list or a thrown
|
|
30
33
|
// error boundary. Distinct from the empty state (which only shows when the fetch SUCCEEDED with no rows).
|
|
31
34
|
const error = ref<string | null>(null)
|
|
@@ -37,12 +40,13 @@ export function useListRows(opts: ListRowsOptions) {
|
|
|
37
40
|
const mine = ++seq
|
|
38
41
|
const locale = opts.locale()
|
|
39
42
|
try {
|
|
40
|
-
const res = await $fetch<ListResponse>(`/api/${opts.collection.value}`, {
|
|
43
|
+
const res = await $fetch<ListResponse>(`/api/${opts.collection.value}/readMany`, {
|
|
41
44
|
query: { ...opts.effectiveQuery.value, ...(locale ? { locale } : {}) },
|
|
42
45
|
})
|
|
43
46
|
if (mine !== seq) return
|
|
44
47
|
rows.value = res.data
|
|
45
48
|
total.value = res.total
|
|
49
|
+
quarantinedCount.value = res.quarantinedCount ?? 0
|
|
46
50
|
error.value = null
|
|
47
51
|
// A page that emptied out from under us — e.g. bulk-deleting the last rows of the trailing page —
|
|
48
52
|
// would otherwise strand the user on an out-of-range page showing the "create your first" empty
|
|
@@ -60,9 +64,10 @@ export function useListRows(opts: ListRowsOptions) {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
return { rows, total, error, totalPages, fetchRows } as {
|
|
67
|
+
return { rows, total, quarantinedCount, error, totalPages, fetchRows } as {
|
|
64
68
|
rows: Ref<Record<string, unknown>[]>
|
|
65
69
|
total: Ref<number>
|
|
70
|
+
quarantinedCount: Ref<number>
|
|
66
71
|
error: Ref<string | null>
|
|
67
72
|
totalPages: ComputedRef<number>
|
|
68
73
|
fetchRows: () => Promise<void>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Ref } from 'vue'
|
|
2
2
|
import type { ListColumn } from '../utils/list-columns'
|
|
3
3
|
import { toQuery, toggleSort, parseListQuery, type FilterCell, type ListState } from '../utils/list-query'
|
|
4
|
-
import { clampPerPage, DEFAULT_PER_PAGE } from '
|
|
4
|
+
import { clampPerPage, DEFAULT_PER_PAGE } from '@michaelthielemann/kestrel-core/client'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The URL is the SINGLE SOURCE OF TRUTH for a collection list's committed state (sort · page · perPage ·
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ref, toValue } from 'vue'
|
|
2
2
|
import type { MaybeRefOrGetter } from 'vue'
|
|
3
3
|
|
|
4
|
-
/** The live publish state of a record's static page, as returned by `GET /api/
|
|
4
|
+
/** The live publish state of a record's static page, as returned by `GET /api/publishStatus`. */
|
|
5
5
|
export interface PublishStatusData {
|
|
6
6
|
route: string | null
|
|
7
7
|
status: 'success' | 'error' | null
|
|
@@ -59,7 +59,7 @@ export function usePublishStatus(args: {
|
|
|
59
59
|
return
|
|
60
60
|
}
|
|
61
61
|
try {
|
|
62
|
-
const res = await $fetch<PublishStatusData>('/api/
|
|
62
|
+
const res = await $fetch<PublishStatusData>('/api/publishStatus', {
|
|
63
63
|
query: { collection: toValue(args.collection), id, locale: toValue(args.locale) },
|
|
64
64
|
})
|
|
65
65
|
if (token === pollToken) data.value = res
|
|
@@ -43,8 +43,8 @@ const heading = computed(() =>
|
|
|
43
43
|
: t('editor.editRecord', { collection: singular.value, id }),
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
// The external-tab button
|
|
47
|
-
//
|
|
46
|
+
// The external-tab button opens a preview ticket when there are unsaved edits (no save, no publish),
|
|
47
|
+
// otherwise the record's own URL — so the tooltip has to say which of the two you'd get.
|
|
48
48
|
const previewTitle = computed(() => t(editorRef.value?.dirty ? 'editor.previewUnsaved' : 'editor.openInNewTab'))
|
|
49
49
|
|
|
50
50
|
const skipGuard = ref(false)
|
|
@@ -121,9 +121,10 @@ async function confirmDelete() {
|
|
|
121
121
|
<KestrelUiButton type="button" variant="ghost" size="sm" icon="external-link" :title="previewTitle" :aria-label="previewTitle" :loading="editorRef?.previewOpening" @click="editorRef?.openPreview()" />
|
|
122
122
|
<KestrelUiButton type="button" variant="secondary" size="sm" icon="x" :disabled="saving" @click="toList">{{ t('common.cancel') }}</KestrelUiButton>
|
|
123
123
|
<KestrelUiButton v-if="id !== 'new'" variant="danger" size="sm" icon="trash" :loading="deleting" @click="onDelete">{{ t('common.delete') }}</KestrelUiButton>
|
|
124
|
-
|
|
124
|
+
<!-- A quarantined record is read-only: no Save, no Publish — there is nothing valid to write. -->
|
|
125
|
+
<KestrelUiButton v-if="!editorRef?.quarantined" type="submit" :form="EDITOR_FORM_ID" variant="primary" size="sm" icon="check" :loading="saving">{{ t('common.save') }}</KestrelUiButton>
|
|
125
126
|
<!-- Publishing is its own decision: Save persists, Publish writes the static page (ADR-0008). -->
|
|
126
|
-
<KestrelUiButton v-if="editorRef?.canPublish !== false" type="button" variant="secondary" size="sm" icon="upload" :loading="editorRef?.publishing" :disabled="saving" @click="editorRef?.publish()">{{ t('common.publish') }}</KestrelUiButton>
|
|
127
|
+
<KestrelUiButton v-if="editorRef?.canPublish !== false && !editorRef?.quarantined" type="button" variant="secondary" size="sm" icon="upload" :loading="editorRef?.publishing" :disabled="saving" @click="editorRef?.publish()">{{ t('common.publish') }}</KestrelUiButton>
|
|
127
128
|
<KestrelEditorStatus class="record__ampel" :dirty="editorRef?.dirty ?? false" :saving="saving" :has-status="editorRef?.hasStatus ?? false" :status="editorRef?.status" :saved-status="editorRef?.savedStatus" :page-like="editorRef?.pageLike ?? false" :live="editorRef?.live" />
|
|
128
129
|
</div>
|
|
129
130
|
</div>
|
|
@@ -17,7 +17,7 @@ try {
|
|
|
17
17
|
// Broken-reference count for the alert banner — fire-and-forget so it never delays the dashboard, and a
|
|
18
18
|
// failure (or a not-yet-built index) simply shows no banner.
|
|
19
19
|
const brokenCount = ref(0)
|
|
20
|
-
$fetch<unknown[]>('/api/
|
|
20
|
+
$fetch<unknown[]>('/api/brokenRefs')
|
|
21
21
|
.then((r) => { brokenCount.value = r.length })
|
|
22
22
|
.catch(() => { brokenCount.value = 0 })
|
|
23
23
|
</script>
|
|
@@ -4,7 +4,7 @@ definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
|
4
4
|
// The global broken-references report: every record that links to a deleted or unpublished target. The
|
|
5
5
|
// referrer keeps its last published output (the stale link stays live) until it is re-edited — this is
|
|
6
6
|
// the editor's queue for fixing those. Derived on read, so an entry clears once the link is fixed or the
|
|
7
|
-
// target restored. Admin-only (the `
|
|
7
|
+
// target restored. Admin-only (the `brokenRefs` pipeline is outside the public set).
|
|
8
8
|
interface BrokenRow {
|
|
9
9
|
source: { collection: string; id: number }
|
|
10
10
|
target: { collection: string; id: number }
|
|
@@ -17,7 +17,7 @@ const broken = ref<BrokenRow[]>([])
|
|
|
17
17
|
// the green all-clear (that would imply a verified-clean site when nothing was actually verified).
|
|
18
18
|
const loadError = ref(false)
|
|
19
19
|
try {
|
|
20
|
-
broken.value = await $fetch<BrokenRow[]>('/api/
|
|
20
|
+
broken.value = await $fetch<BrokenRow[]>('/api/brokenRefs')
|
|
21
21
|
} catch {
|
|
22
22
|
loadError.value = true
|
|
23
23
|
}
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
// Wire shapes for the collection batch endpoint, shared by the ops composable and the delete dialog.
|
|
2
2
|
// Mirrors media/app/utils/ops.ts: one definition, no drift between the caller and the confirm UI.
|
|
3
|
+
import type { SerializedAction } from '@michaelthielemann/kestrel-core'
|
|
3
4
|
|
|
4
5
|
/** The four batch actions the command endpoint accepts (a row action is a bulk op with one id). */
|
|
5
6
|
export type BatchAction = 'delete' | 'publish' | 'unpublish' | 'duplicate'
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
+
/** The built-in actions the admin renders with its own dedicated presentation (Delete/Duplicate buttons,
|
|
9
|
+
* the Publish/Unpublish pair) — always present on the wire, but never re-rendered generically. */
|
|
10
|
+
const BUILTIN_ACTION_NAMES = new Set(['deleteMany', 'duplicate'])
|
|
11
|
+
|
|
12
|
+
/** The schema-driven actions beyond the built-ins — what a consumer's `definePipeline` adds. */
|
|
13
|
+
export function customActions(actions: SerializedAction[]): SerializedAction[] {
|
|
14
|
+
return actions.filter((a) => !BUILTIN_ACTION_NAMES.has(a.name))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Custom actions invokable from the bulk-selection bar. */
|
|
18
|
+
export function bulkCustomActions(actions: SerializedAction[]): SerializedAction[] {
|
|
19
|
+
return customActions(actions).filter((a) => a.kind === 'bulk' || a.kind === 'both')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Custom actions invokable from a single row. */
|
|
23
|
+
export function recordCustomActions(actions: SerializedAction[]): SerializedAction[] {
|
|
24
|
+
return customActions(actions).filter((a) => a.kind === 'record' || a.kind === 'both')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a batch pipeline answers with: `deleteMany`/`updateMany` a count plus the ids they touched,
|
|
28
|
+
* `duplicate` the created rows. */
|
|
29
|
+
export type BatchPipelineResult = { count: number, ids: number[] } | { id: number }[]
|
|
30
|
+
|
|
31
|
+
/** The batch outcome the list UI reports. For `duplicate`, `ids` are the CREATED rows. A custom action's
|
|
32
|
+
* `action` is its pipeline name — not one of the four built-ins. */
|
|
8
33
|
export interface BulkResult {
|
|
9
|
-
action: BatchAction
|
|
34
|
+
action: BatchAction | (string & {})
|
|
10
35
|
count: number
|
|
11
36
|
ids: number[]
|
|
12
37
|
}
|
|
13
38
|
|
|
14
|
-
/** GET /api/
|
|
39
|
+
/** GET /api/{collection}/referrers?ids=1,2,3 response: id -> how many other records reference it.
|
|
15
40
|
* `checked` is false when the index lookup itself failed, in which case `counts` is empty because nothing
|
|
16
41
|
* could be read — NOT because nothing links to the selection. */
|
|
17
42
|
export interface ReferrerCounts {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { FieldDef } from '
|
|
2
|
-
import type { SerializedField } from '../../../core/server/utils/serialize-collection'
|
|
1
|
+
import type { FieldDef, SerializedField } from '@michaelthielemann/kestrel-core'
|
|
3
2
|
import { resolveFieldEmpty } from '../../../ui/app/utils/field-empty'
|
|
4
3
|
|
|
5
4
|
export interface ServerIssue {
|
|
@@ -208,6 +207,39 @@ function flattenBlocksById(blocks: readonly unknown[] | null | undefined, into:
|
|
|
208
207
|
}
|
|
209
208
|
}
|
|
210
209
|
|
|
210
|
+
/** One stale reference a record holds. Wire shape of `DeadRef` (kept structural here so this module stays
|
|
211
|
+
* Vue-free and doesn't import the admin-only `dead-refs.ts` type file). */
|
|
212
|
+
interface DeadRefLike {
|
|
213
|
+
field: string
|
|
214
|
+
blockId?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Carry dead-reference warnings across a `content` change, mirroring `reconcileBlockErrors`: a warning at
|
|
219
|
+
* a block (by stable id) is dropped once THAT field's value changes (the user swapped the broken
|
|
220
|
+
* reference) or the block is removed; it survives a pure reorder. Root-level (non-block) dead refs are
|
|
221
|
+
* untouched here — `setField` drops those directly when their own field is edited.
|
|
222
|
+
*/
|
|
223
|
+
export function reconcileDeadRefs<T extends DeadRefLike>(
|
|
224
|
+
refs: readonly T[],
|
|
225
|
+
prev: readonly unknown[] | null | undefined,
|
|
226
|
+
next: readonly unknown[] | null | undefined,
|
|
227
|
+
): T[] {
|
|
228
|
+
const prevById = new Map<string, BlockLike>()
|
|
229
|
+
const nextById = new Map<string, BlockLike>()
|
|
230
|
+
flattenBlocksById(prev, prevById)
|
|
231
|
+
flattenBlocksById(next, nextById)
|
|
232
|
+
return refs.filter((r) => {
|
|
233
|
+
if (!r.blockId) return true
|
|
234
|
+
const before = prevById.get(r.blockId)
|
|
235
|
+
const after = nextById.get(r.blockId)
|
|
236
|
+
if (!before || !after) return false
|
|
237
|
+
const beforeVal = (before.props ?? {})[r.field]
|
|
238
|
+
const afterVal = (after.props ?? {})[r.field]
|
|
239
|
+
return valuesEqual(beforeVal, afterVal)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
211
243
|
/** Structural deep-equality for the value shapes the editor tracks (primitives, arrays, plain objects). */
|
|
212
244
|
export function valuesEqual(a: unknown, b: unknown): boolean {
|
|
213
245
|
if (a === b) return true
|
|
@@ -31,4 +31,6 @@ export interface EditorExpose {
|
|
|
31
31
|
/** Open the record in a new tab: the saved URL, or the unsaved state carried by a preview ticket. */
|
|
32
32
|
openPreview: () => Promise<void>
|
|
33
33
|
previewOpening: boolean
|
|
34
|
+
/** The loaded row is the quarantine shape — read-only, hosts must hide Save/Publish. */
|
|
35
|
+
quarantined: boolean
|
|
34
36
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
|
2
|
-
import type { FieldDef } from '
|
|
3
|
-
import type { LayoutNode } from '../../../core/server/utils/field-layout'
|
|
2
|
+
import type { FieldDef, LayoutNode } from '@michaelthielemann/kestrel-core'
|
|
4
3
|
import type { BlockErrorMap } from './edit-form'
|
|
5
4
|
import type { DeadRef } from './dead-refs'
|
|
6
5
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SerializedField } from '
|
|
1
|
+
import type { SerializedField } from '@michaelthielemann/kestrel-core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The jsKey (wire key) for a field — NOT a DB column name. The server marks single relation/media fields
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { META_FILTER_KIND, fieldFilterKind } from '@michaelthielemann/kestrel-core/client'
|
|
2
|
+
import type { FilterKind } from '@michaelthielemann/kestrel-core/client'
|
|
3
|
+
import type { SerializedCollection, SerializedField } from '@michaelthielemann/kestrel-core'
|
|
3
4
|
import { jsKey } from './field-keys'
|
|
4
5
|
|
|
5
6
|
export type ListColumnType = 'meta' | 'field' | 'translations' | 'deadRefs'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FILTER_RE, isFilterOp, opAllowed
|
|
2
|
-
import {
|
|
1
|
+
import { FILTER_RE, clampPerPage, isFilterOp, opAllowed } from '@michaelthielemann/kestrel-core/client'
|
|
2
|
+
import type { FilterOp } from '@michaelthielemann/kestrel-core/client'
|
|
3
3
|
import type { ListColumn } from './list-columns'
|
|
4
4
|
|
|
5
5
|
// The page-size bounds live in ONE shared module (core/app/utils/list-limits) so the client list, the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SerializedBlock, SerializedField } from '
|
|
1
|
+
import type { SerializedBlock, SerializedField } from '@michaelthielemann/kestrel-core'
|
|
2
2
|
import type { ResolvedMedia } from '../../../media/app/composables/useMediaResolver'
|
|
3
|
-
import { collectRichtextRefs, resolveRichtextLinks } from '
|
|
3
|
+
import { collectRichtextRefs, resolveRichtextLinks } from '@michaelthielemann/kestrel-core/client'
|
|
4
4
|
|
|
5
5
|
// Single source of truth for the resolved-media shape lives in the media layer (admin → media).
|
|
6
6
|
export type { ResolvedMedia }
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { safeRedirect } from './safe-redirect'
|
|
2
2
|
|
|
3
|
-
// The auth
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
const BOOTSTRAP_PATHS = new Set(['/api/auth/login', '/api/auth/session', '/api/auth/logout'])
|
|
3
|
+
// The auth endpoints (login/session/logout). A 401 from these is not a stale session — a wrong-password
|
|
4
|
+
// login 401s — so it must not trigger a re-auth redirect. Matched exactly, so a guarded endpoint under a
|
|
5
|
+
// consumer collection named `auth` (e.g. `/api/auth/readOne/5`) still re-authenticates.
|
|
6
|
+
const BOOTSTRAP_PATHS = new Set(['/api/login', '/api/session', '/api/logout'])
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Decide whether a failed `$fetch` should bounce the user to the login screen, and to which URL.
|
|
@@ -12,7 +11,7 @@ const BOOTSTRAP_PATHS = new Set(['/api/auth/login', '/api/auth/session', '/api/a
|
|
|
12
11
|
* Returns the login redirect target for a 401 raised by a guarded `/api/*` call made from inside the
|
|
13
12
|
* admin SPA — but stays null for:
|
|
14
13
|
* - non-401 statuses and non-`/api` requests (nothing to re-authenticate);
|
|
15
|
-
* - the auth
|
|
14
|
+
* - the auth endpoints: a wrong-password login 401s and must stay put;
|
|
16
15
|
* - any location that isn't a safe admin redirect target — which also rules out the login page itself
|
|
17
16
|
* (no redirect loop) and public/SSG pages (the static site never logs in).
|
|
18
17
|
*/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
|
|
3
|
+
|
|
4
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
5
|
+
export default defineNitroPlugin(() => {
|
|
6
|
+
for (const def of buildAuthPipelines()) registerPipeline(def)
|
|
7
|
+
})
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import collections from '#kestrel/collections'
|
|
2
2
|
import blocks from '#kestrel/blocks'
|
|
3
|
-
import { registerBlock, type BlockDef } from '
|
|
4
|
-
import { ensureBuilt } from '
|
|
3
|
+
import { registerBlock, type BlockDef } from '@michaelthielemann/kestrel-fields'
|
|
4
|
+
import { ensureBuilt, collectionEnabled, registerCollection } from '@michaelthielemann/kestrel-core'
|
|
5
5
|
import { resolveServerKestrel, serverRuntimeConfig } from '../../../core/server/utils/server-config'
|
|
6
|
-
import {
|
|
7
|
-
import type { CollectionDef } from '../../../core/server/utils/defineCollection'
|
|
8
|
-
import type { BuiltCollection } from '../../../core/server/utils/collection-types'
|
|
9
|
-
|
|
6
|
+
import type { BuiltCollection, CollectionDef } from '@michaelthielemann/kestrel-core'
|
|
10
7
|
// Auto-discovered collection files default-export either a plain CollectionDef (the common consumer
|
|
11
8
|
// form, `export default defineCollection(…)`) or an already-built collection (advanced). `ensureBuilt`
|
|
12
9
|
// normalizes both so the registry only ever holds BuiltCollections.
|
|
13
10
|
//
|
|
14
|
-
// NOTE on ordering:
|
|
15
|
-
//
|
|
16
|
-
// 00.migrate/02.schema-sync
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
11
|
+
// NOTE on ordering: this plugin's real execution position is declared data now
|
|
12
|
+
// (layers/core/modules/plugin-order/plugin-order.ts), not an accident of filename/layer scan order — it
|
|
13
|
+
// runs after every core plugin (00.migrate/02.schema-sync included), identically in both build contexts.
|
|
14
|
+
// It is order-free regardless: nothing reads the registry at plugin init — migrate/schema-sync read the
|
|
15
|
+
// `#kestrel/collections` virtual directly, and the ref/cleanup/publish plugins register deferred listeners.
|
|
16
|
+
// Keep that invariant — do not add an init-time allCollections() read.
|
|
20
17
|
export default defineNitroPlugin(() => {
|
|
21
18
|
for (const block of blocks as BlockDef[]) registerBlock(block)
|
|
22
19
|
// Built-in collections (`pages`, `media`) register by default but can be disabled per consumer via
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { registerFieldPopulator } from '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { getOne } from '../../../core/server/utils/crud'
|
|
5
|
-
|
|
1
|
+
import { getCollection, getOne, registerFieldPopulator, useDb } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
|
|
3
|
+
import { buildRelationFieldPopulator, skipMissing } from '@michaelthielemann/kestrel-collections'
|
|
6
4
|
// Expand relation fields to the fully-populated related record(s) under `$<name>` at read time (depth ≥ 1).
|
|
7
5
|
// The related read is published-only (a draft target is skipped, never leaked to a public read), tolerates a
|
|
8
6
|
// stale/deleted id (getOne 404 → null via `skipMissing`, other errors propagate — fail-loud), and runs at
|
|
9
7
|
// `depth - 1`, so `populateRow` bails at 0 and cycles terminate. Registered per-type; the shared field-tree
|
|
10
8
|
// walker (fields layer) dispatches it.
|
|
11
|
-
// The reachability predicate is the
|
|
12
|
-
//
|
|
9
|
+
// The reachability predicate is the declared public set, NOT the guard's full decision — the guard also
|
|
10
|
+
// folds in `registeredGrants()`, which this omits. So a collection opened to anonymous by a registered
|
|
13
11
|
// grant is served on its own route but stays unexpanded here: fail-closed drift, never a widening.
|
|
14
12
|
export default defineNitroPlugin(() => {
|
|
15
13
|
registerFieldPopulator('relation', buildRelationFieldPopulator((collection, id, depth, locale, publicOnly) => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { basename } from 'node:path'
|
|
3
3
|
import { parse } from '@babel/parser'
|
|
4
|
-
import type { FieldDef } from '
|
|
5
|
-
import * as factoryModule from '
|
|
6
|
-
import { KESTREL_FIELD } from '
|
|
4
|
+
import type { FieldDef } from '@michaelthielemann/kestrel-core'
|
|
5
|
+
import * as factoryModule from '../../app/utils/field-factories'
|
|
6
|
+
import { KESTREL_FIELD } from '../../app/utils/field-factories'
|
|
7
7
|
|
|
8
8
|
/** The extracted block definition (shape mirrors `BlockDef`, kept structural so this build-time module
|
|
9
9
|
* needs no `layers/fields` runtime import). */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { addComponentsDir, addTemplate, addTypeTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
4
|
-
import { collectBlockSfcs, collectDefinitions, renderRegistry } from './scan'
|
|
4
|
+
import { collectBlockSfcs, collectDefinitions, collectManifestFiles, renderPackageConcatRegistry, renderPackageMergedRegistry, renderRegistry } from './scan'
|
|
5
5
|
import { renderBlockRegistry } from './extract-block'
|
|
6
6
|
import { offerableLayouts, renderLayoutRegistry } from '../../app/utils/layouts'
|
|
7
|
+
import { PACKAGE_COLLECTIONS, PACKAGE_MANIFESTS, PACKAGE_SCHEMA_TABLES } from './package-registry'
|
|
7
8
|
|
|
8
9
|
export default defineNuxtModule({
|
|
9
10
|
meta: { name: 'kestrel-auto-discovery' },
|
|
@@ -18,6 +19,7 @@ export default defineNuxtModule({
|
|
|
18
19
|
// Blocks are authored as single `.vue` files under each layer's `app/blocks/` — the SFC is BOTH the
|
|
19
20
|
// schema source (extracted at build into `#kestrel/blocks`) AND the display component. Register the dir
|
|
20
21
|
// so `app/blocks/Hero.vue` resolves as the global `BlocksHero` that the public BlockRenderer renders.
|
|
22
|
+
// No Kestrel package ships a block today, so this stays a pure consumer-dir scan (nothing to bridge).
|
|
21
23
|
for (const root of roots) {
|
|
22
24
|
const dir = join(root, 'app/blocks')
|
|
23
25
|
if (existsSync(dir)) addComponentsDir({ path: dir, prefix: 'Blocks', global: true, pathPrefix: false })
|
|
@@ -34,21 +36,76 @@ export default defineNuxtModule({
|
|
|
34
36
|
|
|
35
37
|
nuxt.hook('nitro:config', (nitro) => {
|
|
36
38
|
nitro.virtual ||= {}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
39
|
+
// `@michaelthielemann/kestrel-fields`'s built-in descriptors (text/richtext/number/…) seed `core`'s field-type registry
|
|
40
|
+
// as a side effect of importing the package itself — core cannot import fields, so this is the one
|
|
41
|
+
// place that import happens. A bare package specifier: no filesystem path-guessing across
|
|
42
|
+
// `nuxt.options._layers` for a `layers/fields` root — the package IS the seed's real, resolvable
|
|
43
|
+
// location, so there is nothing left to guess. Prepended so it always runs, even with zero consumer
|
|
44
|
+
// field types (an empty `collectDefinitions` result would otherwise generate `export default []`,
|
|
45
|
+
// importing nothing). Consumer field types register as a side effect on import too, and the schema
|
|
46
|
+
// engine builds a table the moment a collection/block module loads — so the collections/blocks/
|
|
47
|
+
// schema-tables/module-manifests virtuals ALL import the field-type seed FIRST (every one of them can
|
|
48
|
+
// now reach a package's `buildCollection()` call transitively via `kestrelDiscovery`, per ADR-0029 —
|
|
49
|
+
// an ESM barrel is an eager, whole-module-graph load — so all four need the same guard, not only the
|
|
50
|
+
// two that directly build collections themselves).
|
|
51
|
+
const importFieldTypesSeed = `import { fieldTypes as __kestrelSeed } from '@michaelthielemann/kestrel-fields'\n`
|
|
52
|
+
+ `if (!__kestrelSeed || typeof __kestrelSeed !== 'object') throw new Error('[kestrel] built-in field types failed to seed')\n`
|
|
53
|
+
// A real runtime check on the imported binding, not a bare `import "path"` or a discarded `void x`
|
|
54
|
+
// — Nitro's dev build tree-shakes an import whose binding has no provable effect, silently dropping
|
|
55
|
+
// the seed and breaking every collection. A `throw` Rollup can't prove unreachable survives.
|
|
56
|
+
// MUST be a static `import` (not `import()`): a dynamic import runs as an ordinary statement AFTER
|
|
57
|
+
// every static import of the same module has already evaluated (ESM linking order), which would run
|
|
58
|
+
// it too late — after a collection's own static import has already tried to build its table. A static
|
|
59
|
+
// import evaluates before any sibling static import declared after it, which is what orders this
|
|
60
|
+
// correctly. Every virtual below imports `@michaelthielemann/kestrel-fields` directly for this reason (on top of, not
|
|
61
|
+
// instead of, `#kestrel/field-types` for collections/blocks — chaining virtual-imports-virtual was
|
|
62
|
+
// unreliable in Nitro dev, so the real package import stays even where the virtual ALSO gets imported).
|
|
63
|
+
const importFieldTypesVirtual = `${importFieldTypesSeed}import { default as __kestrelFieldTypes } from '#kestrel/field-types'\n`
|
|
64
|
+
+ `if (!__kestrelFieldTypes) throw new Error('[kestrel] #kestrel/field-types failed to load')\n`
|
|
65
|
+
|
|
40
66
|
nitro.virtual['#kestrel/field-types'] = () =>
|
|
41
|
-
renderRegistry(collectDefinitions(roots, 'server/field-types'))
|
|
67
|
+
`${importFieldTypesSeed}${renderRegistry(collectDefinitions(roots, 'server/field-types'))}`
|
|
68
|
+
|
|
42
69
|
nitro.virtual['#kestrel/collections'] = () =>
|
|
43
|
-
|
|
70
|
+
renderPackageMergedRegistry({
|
|
71
|
+
packages: PACKAGE_COLLECTIONS,
|
|
72
|
+
property: 'collections',
|
|
73
|
+
consumerFiles: collectDefinitions(roots, 'server/collections'),
|
|
74
|
+
nameOfExpr: '(x) => x.name',
|
|
75
|
+
preamble: importFieldTypesVirtual,
|
|
76
|
+
})
|
|
77
|
+
|
|
44
78
|
// Extract each `app/blocks/*.vue` into a plain BlockDef inlined as JSON (no per-file imports needed).
|
|
79
|
+
// No package ships a block today (see above), so this stays the plain consumer-dir scan, prefixed
|
|
80
|
+
// with the same seed a block's field-typed props may need.
|
|
45
81
|
nitro.virtual['#kestrel/blocks'] = () =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
82
|
+
`${importFieldTypesVirtual}${renderBlockRegistry(collectBlockSfcs(roots))}`
|
|
83
|
+
|
|
84
|
+
// Standalone (non-collection) tables: each package's `kestrelDiscovery.schemaTables`, merged with
|
|
85
|
+
// each layer's `server/schema-tables/*.ts` default export (media → folders, public → publish_deps/
|
|
86
|
+
// publish_status/…; consumers can add their own). The schema engine consumes this instead of core
|
|
87
|
+
// hardcoding upper-layer tables.
|
|
50
88
|
nitro.virtual['#kestrel/schema-tables'] = () =>
|
|
51
|
-
|
|
89
|
+
renderPackageMergedRegistry({
|
|
90
|
+
packages: PACKAGE_SCHEMA_TABLES,
|
|
91
|
+
property: 'schemaTables',
|
|
92
|
+
consumerFiles: collectDefinitions(roots, 'server/schema-tables'),
|
|
93
|
+
nameOfExpr: '(x) => __kestrelTableName(x)',
|
|
94
|
+
extraImports: `import { getTableName as __kestrelTableName } from 'drizzle-orm'`,
|
|
95
|
+
preamble: importFieldTypesVirtual,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Per-module ownership manifests (ADR-0012): each package's `kestrelDiscovery.manifest` plus each
|
|
99
|
+
// layer's `server/db/manifest.ts` default export, for the `db:migrate-module` task to enumerate
|
|
100
|
+
// without core importing an upper layer (or a package) directly. No dedup — every contribution is a
|
|
101
|
+
// distinct module (see `collectManifestFiles`'s own TSDoc).
|
|
102
|
+
nitro.virtual['#kestrel/module-manifests'] = () =>
|
|
103
|
+
renderPackageConcatRegistry({
|
|
104
|
+
packages: PACKAGE_MANIFESTS,
|
|
105
|
+
property: 'manifest',
|
|
106
|
+
consumerFiles: collectManifestFiles(roots),
|
|
107
|
+
preamble: importFieldTypesVirtual,
|
|
108
|
+
})
|
|
52
109
|
})
|
|
53
110
|
},
|
|
54
111
|
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kestrel's own packages that ship discoverable content, by category — a package declares WHAT it
|
|
3
|
+
* contributes via its own `kestrelDiscovery` named export (see `@michaelthielemann/kestrel-core`'s `KestrelPackageDiscovery`
|
|
4
|
+
* TSDoc); this module still needs an explicit list of WHICH packages to check, since a Nitro virtual's
|
|
5
|
+
* generated source is plain text written before anything runs — there is no filesystem/`node_modules` scan
|
|
6
|
+
* to discover the list itself. Deliberately per-category, not one flat package list: a virtual only ever
|
|
7
|
+
* imports a package here if that package actually contributes to ITS category, so (for example) building
|
|
8
|
+
* the schema-tables virtual never eagerly touches `@michaelthielemann/kestrel-collections`, which has none.
|
|
9
|
+
*
|
|
10
|
+
* A package with content in a new category is added to the matching list below when that lands — an
|
|
11
|
+
* explicit, reviewable registration, not a guess. `test/architecture/kestrel-discovery.test.ts`'s
|
|
12
|
+
* "bidirectional consistency" suite computes, against every real workspace package's actual
|
|
13
|
+
* `kestrelDiscovery` export, that these three lists are exactly right (nothing missing, nothing stale) —
|
|
14
|
+
* so a package that starts or stops contributing a category and forgets to update the matching list here
|
|
15
|
+
* fails a fast unit test, not a runtime 404 discovered later.
|
|
16
|
+
*/
|
|
17
|
+
export const PACKAGE_COLLECTIONS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-collections', '@michaelthielemann/kestrel-publishing']
|
|
18
|
+
export const PACKAGE_SCHEMA_TABLES = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']
|
|
19
|
+
export const PACKAGE_MANIFESTS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']
|