@michaelthielemann/kestrel 4.1.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
|
|
3
|
-
|
|
4
|
-
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
5
|
-
export default defineNitroPlugin(() => {
|
|
6
|
-
for (const def of buildAuthPipelines()) registerPipeline(def)
|
|
7
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default defineNuxtConfig({})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import collections from '#kestrel/collections'
|
|
2
|
-
import blocks from '#kestrel/blocks'
|
|
3
|
-
import { registerBlock, type BlockDef } from '@michaelthielemann/kestrel-fields'
|
|
4
|
-
import { ensureBuilt, collectionEnabled, registerCollection } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
import { resolveServerKestrel, serverRuntimeConfig } from '../../../core/server/utils/server-config'
|
|
6
|
-
import type { BuiltCollection, CollectionDef } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
// Auto-discovered collection files default-export either a plain CollectionDef (the common consumer
|
|
8
|
-
// form, `export default defineCollection(…)`) or an already-built collection (advanced). `ensureBuilt`
|
|
9
|
-
// normalizes both so the registry only ever holds BuiltCollections.
|
|
10
|
-
//
|
|
11
|
-
// NOTE on ordering: this plugin's real execution position is declared data now
|
|
12
|
-
// (layers/core/modules/plugin-order/plugin-order.ts), not an accident of filename/layer scan order — it
|
|
13
|
-
// runs after every core plugin (00.migrate/02.schema-sync included), identically in both build contexts.
|
|
14
|
-
// It is order-free regardless: nothing reads the registry at plugin init — migrate/schema-sync read the
|
|
15
|
-
// `#kestrel/collections` virtual directly, and the ref/cleanup/publish plugins register deferred listeners.
|
|
16
|
-
// Keep that invariant — do not add an init-time allCollections() read.
|
|
17
|
-
export default defineNitroPlugin(() => {
|
|
18
|
-
for (const block of blocks as BlockDef[]) registerBlock(block)
|
|
19
|
-
// Built-in collections (`pages`, `media`) register by default but can be disabled per consumer via
|
|
20
|
-
// `kestrel: { collections: { <name>: false } }`. Prefer the value the kestrel module put on runtimeConfig
|
|
21
|
-
// (the consumer's `kestrel: {}`); fall back to Kestrel's own resolved config for non-Nitro / dev callers.
|
|
22
|
-
const toggles = (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>
|
|
23
|
-
for (const collection of collections as (CollectionDef | BuiltCollection)[]) {
|
|
24
|
-
const built = ensureBuilt(collection)
|
|
25
|
-
// The shared predicate (schema/bootstrap) — the schema engine gates on exactly the same rule.
|
|
26
|
-
if (!collectionEnabled(built.def, toggles)) continue
|
|
27
|
-
registerCollection(built)
|
|
28
|
-
}
|
|
29
|
-
})
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { getCollection, getOne, registerFieldPopulator, useDb } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
|
|
3
|
-
import { buildRelationFieldPopulator, skipMissing } from '@michaelthielemann/kestrel-collections'
|
|
4
|
-
// Expand relation fields to the fully-populated related record(s) under `$<name>` at read time (depth ≥ 1).
|
|
5
|
-
// The related read is published-only (a draft target is skipped, never leaked to a public read), tolerates a
|
|
6
|
-
// stale/deleted id (getOne 404 → null via `skipMissing`, other errors propagate — fail-loud), and runs at
|
|
7
|
-
// `depth - 1`, so `populateRow` bails at 0 and cycles terminate. Registered per-type; the shared field-tree
|
|
8
|
-
// walker (fields layer) dispatches it.
|
|
9
|
-
// The reachability predicate is the declared public set, NOT the guard's full decision — the guard also
|
|
10
|
-
// folds in `registeredGrants()`, which this omits. So a collection opened to anonymous by a registered
|
|
11
|
-
// grant is served on its own route but stays unexpanded here: fail-closed drift, never a widening.
|
|
12
|
-
export default defineNitroPlugin(() => {
|
|
13
|
-
registerFieldPopulator('relation', buildRelationFieldPopulator((collection, id, depth, locale, publicOnly) => {
|
|
14
|
-
const built = getCollection(collection)
|
|
15
|
-
if (!built) return null
|
|
16
|
-
return skipMissing(() => getOne(useDb(), built, id, depth, locale, true, publicOnly) as Record<string, unknown>)
|
|
17
|
-
}, (collection) => isPubliclyReadable(collection, publicReadableResources())))
|
|
18
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { FieldDef } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Field factories for single-file (SFC) block authoring, à la Pruvious. A block's schema is declared as its
|
|
5
|
-
* component's `defineProps({ heading: textField({ required: true }), … })`; each factory returns a DUAL
|
|
6
|
-
* object — a Vue prop descriptor (`type` = a JS-global constructor) that ALSO carries the Kestrel `FieldDef`
|
|
7
|
-
* under `[KESTREL_FIELD]`. Vue reads `type` at runtime; the build-time SFC extractor reads `[KESTREL_FIELD]`.
|
|
8
|
-
*
|
|
9
|
-
* This file imports NO Vue/Nuxt runtime (only JS globals), so it is importable BOTH in the browser (the SFC's
|
|
10
|
-
* props declaration) AND by the Node extractor that injects these factories to evaluate a block's schema.
|
|
11
|
-
* `Symbol.for` (global registry) keeps the key identical even if the file is instantiated twice.
|
|
12
|
-
*/
|
|
13
|
-
export const KESTREL_FIELD = Symbol.for('kestrel.field')
|
|
14
|
-
|
|
15
|
-
export interface FieldFactoryResult<C = unknown> {
|
|
16
|
-
/** A Vue prop constructor (String / Number / Boolean / Object / Array) — the runtime prop shape. Typed as
|
|
17
|
-
* the specific constructor so Vue's `defineProps` accepts the factory result AND infers the prop's value
|
|
18
|
-
* type (`textField()` → `string`, `mediaField()` → `number`, …). */
|
|
19
|
-
type: C
|
|
20
|
-
/** The Kestrel field definition the extractor lifts into the block's schema. */
|
|
21
|
-
[KESTREL_FIELD]: FieldDef
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Keys that belong on the FieldDef itself (not inside `options`); everything else a factory receives is a
|
|
25
|
-
// type-specific option. Mirrors `BaseFieldDef` in defineCollection.ts.
|
|
26
|
-
const BASE_KEYS = new Set(['required', 'unique', 'label', 'default', 'condition', 'renamedFrom', 'populate'])
|
|
27
|
-
|
|
28
|
-
function partition(opts: Record<string, unknown>): { base: Record<string, unknown>; options: Record<string, unknown> } {
|
|
29
|
-
const base: Record<string, unknown> = {}
|
|
30
|
-
const options: Record<string, unknown> = {}
|
|
31
|
-
for (const [k, v] of Object.entries(opts)) (BASE_KEYS.has(k) ? base : options)[k] = v
|
|
32
|
-
return { base, options }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function wrap<C>(def: Record<string, unknown>, vueType: C): FieldFactoryResult<C> {
|
|
36
|
-
// The factory assembles the FieldDef structurally (open discriminated union); cast through `unknown`.
|
|
37
|
-
return { type: vueType, [KESTREL_FIELD]: def as unknown as FieldDef }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** A leaf field: base props at the top level, everything else under `options`. */
|
|
41
|
-
function leaf<C>(type: string, vueType: C) {
|
|
42
|
-
return (opts: Record<string, unknown> = {}): FieldFactoryResult<C> => {
|
|
43
|
-
const { base, options } = partition(opts)
|
|
44
|
-
const def: Record<string, unknown> = { type, ...base }
|
|
45
|
-
if (Object.keys(options).length) def.options = options
|
|
46
|
-
return wrap(def, vueType)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Generic escape hatch for a CUSTOM field type (registered via `defineFieldType`) that has no dedicated
|
|
51
|
-
* factory — `field('secureGallery', { … })`. Base props split off the top level, the rest into `options`;
|
|
52
|
-
* the Vue prop is typed `Object` (unknown value shape). */
|
|
53
|
-
export function field(type: string, opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor> {
|
|
54
|
-
const { base, options } = partition(opts)
|
|
55
|
-
const def: Record<string, unknown> = { type, ...base }
|
|
56
|
-
if (Object.keys(options).length) def.options = options
|
|
57
|
-
return wrap(def, Object)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const textField = leaf('text', String)
|
|
61
|
-
export const slugField = leaf('slug', String)
|
|
62
|
-
export const richtextField = leaf('richtext', String)
|
|
63
|
-
export const numberField = leaf('number', Number)
|
|
64
|
-
export const booleanField = leaf('boolean', Boolean)
|
|
65
|
-
export const jsonField = leaf('json', Object)
|
|
66
|
-
export const linkField = leaf('link', Object)
|
|
67
|
-
|
|
68
|
-
/** Media: `multiple` (stored bare, an id array) → Array prop; single (a `<name>Id` FK) → Number prop. */
|
|
69
|
-
export function mediaField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
|
|
70
|
-
const { base, options } = partition(opts)
|
|
71
|
-
const def: Record<string, unknown> = { type: 'media', ...base }
|
|
72
|
-
if (Object.keys(options).length) def.options = options
|
|
73
|
-
return wrap(def, options.multiple ? Array : Number)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Choice: `multiple` → the value is a `string[]` (Array prop), single → a `string` (String prop). */
|
|
77
|
-
export function choiceField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | StringConstructor> {
|
|
78
|
-
const { base, options } = partition(opts)
|
|
79
|
-
const def: Record<string, unknown> = { type: 'choice', ...base }
|
|
80
|
-
if (Object.keys(options).length) def.options = options
|
|
81
|
-
return wrap(def, options.multiple ? Array : String)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Datetime: `range` → the value is a `{ start, end }` object (Object prop), else a `string` (String prop). */
|
|
85
|
-
export function datetimeField(opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor | StringConstructor> {
|
|
86
|
-
const { base, options } = partition(opts)
|
|
87
|
-
const def: Record<string, unknown> = { type: 'datetime', ...base }
|
|
88
|
-
if (Object.keys(options).length) def.options = options
|
|
89
|
-
return wrap(def, options.range ? Object : String)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Relation: `collection`/`many`/`labelField` map into `relation`; `many` → Array prop, single → Number. */
|
|
93
|
-
export function relationField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
|
|
94
|
-
const { collection, many, labelField, ...rest } = opts as {
|
|
95
|
-
collection?: string
|
|
96
|
-
many?: boolean
|
|
97
|
-
labelField?: string
|
|
98
|
-
} & Record<string, unknown>
|
|
99
|
-
if (typeof collection !== 'string' || !collection) {
|
|
100
|
-
throw new Error('relationField requires a `collection` (the target collection name)')
|
|
101
|
-
}
|
|
102
|
-
const { base, options } = partition(rest)
|
|
103
|
-
const relation: Record<string, unknown> = { collection }
|
|
104
|
-
if (many) relation.many = many
|
|
105
|
-
if (labelField) relation.labelField = labelField
|
|
106
|
-
const def: Record<string, unknown> = { type: 'relation', ...base, relation }
|
|
107
|
-
if (Object.keys(options).length) def.options = options
|
|
108
|
-
return wrap(def, many ? Array : Number)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Repeater: sub-fields are authored as factory calls too — unwrap each to its FieldDef (recursively).
|
|
112
|
-
* `fieldLayout` (the sub-field editor layout) forwards into `options` instead of being dropped. */
|
|
113
|
-
export function repeaterField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor> {
|
|
114
|
-
const { fields, fieldLayout, ...rest } = opts as { fields?: Record<string, unknown>; fieldLayout?: unknown } & Record<string, unknown>
|
|
115
|
-
const { base } = partition(rest)
|
|
116
|
-
const options: Record<string, unknown> = { fields: unwrapFields(fields ?? {}) }
|
|
117
|
-
if (fieldLayout !== undefined) options.fieldLayout = fieldLayout
|
|
118
|
-
return wrap({ type: 'repeater', ...base, options }, Array)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Convert a `{ name: <factory-result | FieldDef> }` map into a `{ name: FieldDef }` map. A nested repeater's
|
|
122
|
-
* result already carries a fully-unwrapped def, so recursion is implicit. */
|
|
123
|
-
function unwrapFields(fields: Record<string, unknown>): Record<string, FieldDef> {
|
|
124
|
-
const out: Record<string, FieldDef> = {}
|
|
125
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
126
|
-
out[k] = v && typeof v === 'object' && KESTREL_FIELD in v ? (v as FieldFactoryResult)[KESTREL_FIELD] : (v as FieldDef)
|
|
127
|
-
}
|
|
128
|
-
return out
|
|
129
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/** The admin shell. Never offerable for a public record — a page rendered inside it would carry the
|
|
2
|
-
* admin chrome and its own `useHead`. */
|
|
3
|
-
export const ADMIN_LAYOUT = 'admin'
|
|
4
|
-
|
|
5
|
-
/** Nuxt's resolved `app.layouts` entry (`nuxt.options.app.layouts`, filled before the `app:resolve` hook). */
|
|
6
|
-
export interface ResolvedLayout { name: string, file: string }
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The layout names a page may be assigned, from Nuxt's own resolved layout map. Nuxt has already done the
|
|
10
|
-
* layer-ordered, name-first dedup (a consumer's `default.vue` shadows the engine's), so this only filters
|
|
11
|
-
* and sorts — no directory scan of our own.
|
|
12
|
-
*/
|
|
13
|
-
export function offerableLayouts(layouts: Record<string, ResolvedLayout | undefined>): string[] {
|
|
14
|
-
return Object.values(layouts)
|
|
15
|
-
.filter((l): l is ResolvedLayout => !!l && typeof l.file === 'string' && l.file.endsWith('.vue'))
|
|
16
|
-
.map((l) => l.name)
|
|
17
|
-
.filter((name) => name !== ADMIN_LAYOUT)
|
|
18
|
-
.sort()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function renderLayoutRegistry(names: string[]): string {
|
|
22
|
-
return `export const kestrelLayouts = ${JSON.stringify(names)}\n`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Options for the page-layout select. The fallback is one entry with an EMPTY value — an unset column
|
|
27
|
-
* already renders `default`, so offering `default` as its own value would give the editor two controls for
|
|
28
|
-
* one outcome and pin the row to a name the consumer may later rename.
|
|
29
|
-
*/
|
|
30
|
-
export function layoutSelectOptions(names: string[], fallbackLabel: string): { label: string, value: string }[] {
|
|
31
|
-
return [
|
|
32
|
-
{ label: fallbackLabel, value: '' },
|
|
33
|
-
...names.filter((n) => n !== DEFAULT_LAYOUT_NAME).map((n) => ({ label: n, value: n })),
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Mirrors `DEFAULT_LAYOUT` in the public layer; kept local so this util stays dependency-free. */
|
|
38
|
-
const DEFAULT_LAYOUT_NAME = 'default'
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { basename } from 'node:path'
|
|
3
|
-
import { parse } from '@babel/parser'
|
|
4
|
-
import type { FieldDef } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
import * as factoryModule from '../../app/utils/field-factories'
|
|
6
|
-
import { KESTREL_FIELD } from '../../app/utils/field-factories'
|
|
7
|
-
|
|
8
|
-
/** The extracted block definition (shape mirrors `BlockDef`, kept structural so this build-time module
|
|
9
|
-
* needs no `layers/fields` runtime import). */
|
|
10
|
-
export interface ExtractedBlock {
|
|
11
|
-
name: string
|
|
12
|
-
fields: Record<string, FieldDef>
|
|
13
|
-
slots?: string[]
|
|
14
|
-
label?: unknown
|
|
15
|
-
icon?: string
|
|
16
|
-
image?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Only the callable factories are injected into the schema-eval scope (exclude the KESTREL_FIELD symbol).
|
|
20
|
-
const FACTORIES: Record<string, unknown> = Object.fromEntries(
|
|
21
|
-
Object.entries(factoryModule).filter(([, v]) => typeof v === 'function'),
|
|
22
|
-
)
|
|
23
|
-
// The factory functions themselves, to catch an uncalled factory (`heading: textField` vs `textField()`).
|
|
24
|
-
const FACTORY_SET = new Set(Object.values(FACTORIES))
|
|
25
|
-
|
|
26
|
-
/** `Hero.vue` → `hero`, `BoxedContainer.vue` → `boxedContainer` (camel-case; `BlockRenderer` pascal-cases it
|
|
27
|
-
* back to the `Blocks<Name>` component). */
|
|
28
|
-
export function blockNameFromFile(fileBase: string): string {
|
|
29
|
-
const name = fileBase.replace(/\.vue$/, '')
|
|
30
|
-
return name.charAt(0).toLowerCase() + name.slice(1)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** The raw `<script setup>` source. `setup` is matched in ANY attribute position (`<script setup lang="ts">`
|
|
34
|
-
* and `<script lang="ts" setup>` are both valid), so a plain `<script>` block (no `setup` word) is skipped.
|
|
35
|
-
* (A real Vite build could use `@vue/compiler-sfc parse` for full robustness — e.g. a `</script>` inside a
|
|
36
|
-
* string literal; a scoped match is adequate + dependency-light here.) */
|
|
37
|
-
function scriptSetup(sfc: string, where: string): string {
|
|
38
|
-
const m = sfc.match(/<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/)
|
|
39
|
-
if (!m) throw new Error(`${where}: a Kestrel block SFC must have a <script setup> block`)
|
|
40
|
-
return m[1]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface Node { type: string; start: number; end: number; [k: string]: unknown }
|
|
44
|
-
|
|
45
|
-
/** Peel the wrappers a macro call is commonly written behind so the CallExpression underneath is reached:
|
|
46
|
-
* a TS as-cast / satisfies (`defineProps({…}) as T`), a non-null assertion (`defineProps({…})!`), and
|
|
47
|
-
* parentheses. */
|
|
48
|
-
function unwrapExpr(node: unknown): Record<string, unknown> | undefined {
|
|
49
|
-
let n = node as Record<string, unknown> | undefined
|
|
50
|
-
while (n && (n.type === 'TSAsExpression' || n.type === 'TSSatisfiesExpression' || n.type === 'TSNonNullExpression' || n.type === 'ParenthesizedExpression')) {
|
|
51
|
-
n = n.expression as Record<string, unknown> | undefined
|
|
52
|
-
}
|
|
53
|
-
return n
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** If `call` resolves to `<macro>(…)`, its first arg node + whether it carried a type parameter
|
|
57
|
-
* (`defineProps<T>()`, unsupported — no runtime schema). Also looks INSIDE `withDefaults(defineProps(…), …)`
|
|
58
|
-
* — the most common props idiom — so that form is not silently extracted as an empty schema. */
|
|
59
|
-
function macroInCall(call: unknown, macro: string): { arg?: Node; hasTypeParam: boolean } | undefined {
|
|
60
|
-
const n = unwrapExpr(call)
|
|
61
|
-
if (n?.type !== 'CallExpression') return undefined
|
|
62
|
-
const callee = n.callee as Record<string, unknown> | undefined
|
|
63
|
-
if (callee?.type === 'Identifier' && callee.name === macro) {
|
|
64
|
-
return { arg: (n.arguments as Node[])[0], hasTypeParam: !!(n.typeParameters || n.typeArguments) }
|
|
65
|
-
}
|
|
66
|
-
// withDefaults(defineProps(…), { … }) — recurse into its arguments to find the wrapped macro call.
|
|
67
|
-
if (callee?.type === 'Identifier' && callee.name === 'withDefaults') {
|
|
68
|
-
for (const a of (n.arguments as unknown[]) ?? []) {
|
|
69
|
-
const hit = macroInCall(a, macro)
|
|
70
|
-
if (hit) return hit
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return undefined
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** The FIRST TOP-LEVEL `<macro>(…)` call — a bare `defineBlock({…})` (ExpressionStatement), a
|
|
77
|
-
* `const props = defineProps({…})` (VariableDeclaration init, incl. `export const` and wrapper forms).
|
|
78
|
-
* Vue's macros are top-level by contract, so scanning only `program.body` (not a deep DFS) means a call
|
|
79
|
-
* nested in a helper function can't shadow it. */
|
|
80
|
-
function macroCall(ast: unknown, macro: string): { arg?: Node; hasTypeParam: boolean } | undefined {
|
|
81
|
-
const body = (ast as { program?: { body?: unknown[] } }).program?.body
|
|
82
|
-
if (!Array.isArray(body)) return undefined
|
|
83
|
-
for (const raw of body as Array<Record<string, unknown>>) {
|
|
84
|
-
// Unwrap `export const props = …` → the VariableDeclaration it wraps.
|
|
85
|
-
const stmt = raw.type === 'ExportNamedDeclaration' && raw.declaration ? (raw.declaration as Record<string, unknown>) : raw
|
|
86
|
-
if (stmt.type === 'ExpressionStatement') {
|
|
87
|
-
const hit = macroInCall(stmt.expression, macro)
|
|
88
|
-
if (hit) return hit
|
|
89
|
-
} else if (stmt.type === 'VariableDeclaration') {
|
|
90
|
-
for (const d of (stmt.declarations as Array<Record<string, unknown>>) ?? []) {
|
|
91
|
-
const hit = macroInCall(d.init, macro)
|
|
92
|
-
if (hit) return hit
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return undefined
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function evalObjectExpr(content: string, node: Node, scope: Record<string, unknown>, where: string): Record<string, unknown> {
|
|
100
|
-
const src = content.slice(node.start, node.end)
|
|
101
|
-
const names = Object.keys(scope)
|
|
102
|
-
try {
|
|
103
|
-
// `src` is a slice of the CONSUMER's own block-SFC source, evaluated once here at build/discovery time
|
|
104
|
-
// to recover the object literal's runtime shape — never attacker- or request-supplied, so this is not
|
|
105
|
-
// an eval-injection surface.
|
|
106
|
-
const fn = new Function(...names, `return (${src})`)
|
|
107
|
-
const out = fn(...names.map((k) => scope[k]))
|
|
108
|
-
if (!out || typeof out !== 'object') throw new Error('expected an object literal')
|
|
109
|
-
return out as Record<string, unknown>
|
|
110
|
-
} catch (e) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`${where}: could not evaluate the block declaration. Field/block args must be self-contained literals + ` +
|
|
113
|
-
`field-factory calls (no imported constants, computed values, or TS type-args). Cause: ${(e as Error).message}`,
|
|
114
|
-
{ cause: e },
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Extract a `BlockDef` from a block SFC's source: `defineProps({ … field factories … })` → `fields`,
|
|
121
|
-
* `defineBlock({ label, slots, icon })` → metadata, `name` ← filename. Static (no component execution): the
|
|
122
|
-
* `defineProps`/`defineBlock` argument object-literals are sliced from source and evaluated with the field
|
|
123
|
-
* factories in scope; each field's `[KESTREL_FIELD]` is lifted into the schema.
|
|
124
|
-
*/
|
|
125
|
-
export function extractBlockDef(sfcSource: string, fileBase: string): ExtractedBlock {
|
|
126
|
-
const name = blockNameFromFile(fileBase)
|
|
127
|
-
const content = scriptSetup(sfcSource, fileBase)
|
|
128
|
-
let ast: ReturnType<typeof parse>
|
|
129
|
-
try {
|
|
130
|
-
ast = parse(content, { sourceType: 'module', plugins: ['typescript'] })
|
|
131
|
-
} catch (e) {
|
|
132
|
-
throw new Error(`${fileBase}: could not parse <script setup> — ${(e as Error).message}`, { cause: e })
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const props = macroCall(ast, 'defineProps')
|
|
136
|
-
if (props?.hasTypeParam && !props.arg) {
|
|
137
|
-
throw new Error(`${fileBase}: use the runtime form defineProps({ … }) with field factories, not defineProps<T>() — the type-only form carries no schema`)
|
|
138
|
-
}
|
|
139
|
-
const propsObj = props?.arg ? evalObjectExpr(content, props.arg, FACTORIES, fileBase) : {}
|
|
140
|
-
|
|
141
|
-
const block = macroCall(ast, 'defineBlock')
|
|
142
|
-
const meta = block?.arg ? evalObjectExpr(content, block.arg, {}, fileBase) : {}
|
|
143
|
-
|
|
144
|
-
const fields: Record<string, FieldDef> = {}
|
|
145
|
-
for (const [key, value] of Object.entries(propsObj)) {
|
|
146
|
-
const carried = value && typeof value === 'object' ? (value as Record<symbol, unknown>)[KESTREL_FIELD] : undefined
|
|
147
|
-
if (carried === undefined) {
|
|
148
|
-
// An UNCALLED field factory (`heading: textField` instead of `textField()`) evaluates to the factory
|
|
149
|
-
// function itself — catch that common slip loudly rather than silently dropping the field.
|
|
150
|
-
if (typeof value === 'function' && FACTORY_SET.has(value)) {
|
|
151
|
-
throw new Error(`${fileBase}: prop "${key}" is a field factory that was not called — write ${(value as { name?: string }).name ?? 'xField'}({ … })`)
|
|
152
|
-
}
|
|
153
|
-
// Otherwise it's a plain (display-only) Vue prop (e.g. `media: Object`, the server-resolved $media bag
|
|
154
|
-
// BlockRenderer passes) — not part of the block schema. A misspelled factory throws at eval time above.
|
|
155
|
-
continue
|
|
156
|
-
}
|
|
157
|
-
const def = carried as FieldDef & { default?: unknown }
|
|
158
|
-
if (typeof def.default === 'function') {
|
|
159
|
-
// The registry is inlined as JSON (renderBlockRegistry) — a function default would be silently dropped.
|
|
160
|
-
throw new Error(`${fileBase}: prop "${key}" has a function \`default\` — block field defaults must be JSON-serializable literals`)
|
|
161
|
-
}
|
|
162
|
-
fields[key] = carried as FieldDef
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const out: ExtractedBlock = { name, fields }
|
|
166
|
-
if (meta.label !== undefined) out.label = meta.label
|
|
167
|
-
if (Array.isArray(meta.slots) && meta.slots.length) out.slots = meta.slots as string[]
|
|
168
|
-
if (typeof meta.icon === 'string') out.icon = meta.icon
|
|
169
|
-
if (typeof meta.image === 'string') out.image = meta.image
|
|
170
|
-
return out
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** The `#kestrel/blocks` virtual body: extract each block SFC and inline the definitions as JSON data
|
|
174
|
-
* (the block registry is plain, JSON-safe data — no imports needed beyond the field-type side effect the
|
|
175
|
-
* caller prepends). */
|
|
176
|
-
export function renderBlockRegistry(vuePaths: string[]): string {
|
|
177
|
-
const defs = vuePaths.map((p) => extractBlockDef(readFileSync(p, 'utf8'), basename(p)))
|
|
178
|
-
return `export default ${JSON.stringify(defs)}`
|
|
179
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { addComponentsDir, addTemplate, addTypeTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
4
|
-
import { collectBlockSfcs, collectDefinitions, collectManifestFiles, renderPackageConcatRegistry, renderPackageMergedRegistry, renderRegistry, resolvePackageEntry } from './scan'
|
|
5
|
-
import { renderBlockRegistry } from './extract-block'
|
|
6
|
-
import { offerableLayouts, renderLayoutRegistry } from '../../app/utils/layouts'
|
|
7
|
-
import { PACKAGE_COLLECTIONS, PACKAGE_MANIFESTS, PACKAGE_SCHEMA_TABLES } from './package-registry'
|
|
8
|
-
|
|
9
|
-
export default defineNuxtModule({
|
|
10
|
-
meta: { name: 'kestrel-auto-discovery' },
|
|
11
|
-
setup(_options, nuxt) {
|
|
12
|
-
const roots = nuxt.options._layers.map((layer) => layer.cwd)
|
|
13
|
-
// The `#kestrel/*` registries are Nitro VIRTUAL modules (runtime only) — tsc can't see their shape, so
|
|
14
|
-
// server code importing them would be `Cannot find module`. Register the ambient declarations into BOTH
|
|
15
|
-
// the Nitro and Nuxt tsconfigs so the typecheck resolves them.
|
|
16
|
-
const { resolve } = createResolver(import.meta.url)
|
|
17
|
-
addTypeTemplate({ filename: 'types/kestrel-virtual.d.ts', src: resolve('./virtual.d.ts') }, { nitro: true, nuxt: true })
|
|
18
|
-
|
|
19
|
-
// Blocks are authored as single `.vue` files under each layer's `app/blocks/` — the SFC is BOTH the
|
|
20
|
-
// schema source (extracted at build into `#kestrel/blocks`) AND the display component. Register the dir
|
|
21
|
-
// so `app/blocks/Hero.vue` resolves as the global `BlocksHero` that the public BlockRenderer renders.
|
|
22
|
-
// No Kestrel package ships a block today, so this stays a pure consumer-dir scan (nothing to bridge).
|
|
23
|
-
for (const root of roots) {
|
|
24
|
-
const dir = join(root, 'app/blocks')
|
|
25
|
-
if (existsSync(dir)) addComponentsDir({ path: dir, prefix: 'Blocks', global: true, pathPrefix: false })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Layouts need no scan of our own: Nuxt already resolves `app/layouts/*.vue` across the layers with the
|
|
29
|
-
// same name-first, consumer-wins dedup, and fills `app.layouts` just before `app:resolve` — which runs
|
|
30
|
-
// inside `generateApp`, ahead of the templates being written, so the closure below is filled in time.
|
|
31
|
-
let layoutNames: string[] = []
|
|
32
|
-
nuxt.hook('app:resolve', (app) => { layoutNames = offerableLayouts(app.layouts ?? {}) })
|
|
33
|
-
// `write` so the resolved list is inspectable in `.nuxt/` — a virtual-only template makes "which layouts
|
|
34
|
-
// did the build actually find" unanswerable without a debugger.
|
|
35
|
-
addTemplate({ filename: 'kestrel-layouts.mjs', write: true, getContents: () => renderLayoutRegistry(layoutNames) })
|
|
36
|
-
|
|
37
|
-
nuxt.hook('nitro:config', (nitro) => {
|
|
38
|
-
nitro.virtual ||= {}
|
|
39
|
-
// `@michaelthielemann/kestrel-fields`'s built-in descriptors (text/richtext/number/…) seed `core`'s field-type registry
|
|
40
|
-
// as a side effect of importing the package itself — core cannot import fields, so this is the one
|
|
41
|
-
// place that import happens. Resolved to a real file path (`resolvePackageEntry`), not left as the
|
|
42
|
-
// bare specifier: a Nitro virtual module has no real file of its own for a bundler to resolve a bare
|
|
43
|
-
// import FROM, so it falls back to the project root — where, under a real consumer's pnpm install,
|
|
44
|
-
// this package isn't visible (only the engine directly depends on it). Prepended so it always runs, even with zero consumer
|
|
45
|
-
// field types (an empty `collectDefinitions` result would otherwise generate `export default []`,
|
|
46
|
-
// importing nothing). Consumer field types register as a side effect on import too, and the schema
|
|
47
|
-
// engine builds a table the moment a collection/block module loads — so the collections/blocks/
|
|
48
|
-
// schema-tables/module-manifests virtuals ALL import the field-type seed FIRST (every one of them can
|
|
49
|
-
// now reach a package's `buildCollection()` call transitively via `kestrelDiscovery`, per ADR-0029 —
|
|
50
|
-
// an ESM barrel is an eager, whole-module-graph load — so all four need the same guard, not only the
|
|
51
|
-
// two that directly build collections themselves).
|
|
52
|
-
const importFieldTypesSeed = `import { fieldTypes as __kestrelSeed } from ${JSON.stringify(resolvePackageEntry('@michaelthielemann/kestrel-fields'))}\n`
|
|
53
|
-
+ `if (!__kestrelSeed || typeof __kestrelSeed !== 'object') throw new Error('[kestrel] built-in field types failed to seed')\n`
|
|
54
|
-
// A real runtime check on the imported binding, not a bare `import "path"` or a discarded `void x`
|
|
55
|
-
// — Nitro's dev build tree-shakes an import whose binding has no provable effect, silently dropping
|
|
56
|
-
// the seed and breaking every collection. A `throw` Rollup can't prove unreachable survives.
|
|
57
|
-
// MUST be a static `import` (not `import()`): a dynamic import runs as an ordinary statement AFTER
|
|
58
|
-
// every static import of the same module has already evaluated (ESM linking order), which would run
|
|
59
|
-
// it too late — after a collection's own static import has already tried to build its table. A static
|
|
60
|
-
// import evaluates before any sibling static import declared after it, which is what orders this
|
|
61
|
-
// correctly. Every virtual below imports `@michaelthielemann/kestrel-fields` directly for this reason (on top of, not
|
|
62
|
-
// instead of, `#kestrel/field-types` for collections/blocks — chaining virtual-imports-virtual was
|
|
63
|
-
// unreliable in Nitro dev, so the real package import stays even where the virtual ALSO gets imported).
|
|
64
|
-
const importFieldTypesVirtual = `${importFieldTypesSeed}import { default as __kestrelFieldTypes } from '#kestrel/field-types'\n`
|
|
65
|
-
+ `if (!__kestrelFieldTypes) throw new Error('[kestrel] #kestrel/field-types failed to load')\n`
|
|
66
|
-
|
|
67
|
-
nitro.virtual['#kestrel/field-types'] = () =>
|
|
68
|
-
`${importFieldTypesSeed}${renderRegistry(collectDefinitions(roots, 'server/field-types'))}`
|
|
69
|
-
|
|
70
|
-
nitro.virtual['#kestrel/collections'] = () =>
|
|
71
|
-
renderPackageMergedRegistry({
|
|
72
|
-
packages: PACKAGE_COLLECTIONS.map(resolvePackageEntry),
|
|
73
|
-
property: 'collections',
|
|
74
|
-
consumerFiles: collectDefinitions(roots, 'server/collections'),
|
|
75
|
-
nameOfExpr: '(x) => x.name',
|
|
76
|
-
preamble: importFieldTypesVirtual,
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
// Extract each `app/blocks/*.vue` into a plain BlockDef inlined as JSON (no per-file imports needed).
|
|
80
|
-
// No package ships a block today (see above), so this stays the plain consumer-dir scan, prefixed
|
|
81
|
-
// with the same seed a block's field-typed props may need.
|
|
82
|
-
nitro.virtual['#kestrel/blocks'] = () =>
|
|
83
|
-
`${importFieldTypesVirtual}${renderBlockRegistry(collectBlockSfcs(roots))}`
|
|
84
|
-
|
|
85
|
-
// Standalone (non-collection) tables: each package's `kestrelDiscovery.schemaTables`, merged with
|
|
86
|
-
// each layer's `server/schema-tables/*.ts` default export (media → folders, public → publish_deps/
|
|
87
|
-
// publish_status/…; consumers can add their own). The schema engine consumes this instead of core
|
|
88
|
-
// hardcoding upper-layer tables.
|
|
89
|
-
nitro.virtual['#kestrel/schema-tables'] = () =>
|
|
90
|
-
renderPackageMergedRegistry({
|
|
91
|
-
packages: PACKAGE_SCHEMA_TABLES.map(resolvePackageEntry),
|
|
92
|
-
property: 'schemaTables',
|
|
93
|
-
consumerFiles: collectDefinitions(roots, 'server/schema-tables'),
|
|
94
|
-
nameOfExpr: '(x) => __kestrelTableName(x)',
|
|
95
|
-
extraImports: `import { getTableName as __kestrelTableName } from 'drizzle-orm'`,
|
|
96
|
-
preamble: importFieldTypesVirtual,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Per-module ownership manifests (ADR-0012): each package's `kestrelDiscovery.manifest` plus each
|
|
100
|
-
// layer's `server/db/manifest.ts` default export, for the `db:migrate-module` task to enumerate
|
|
101
|
-
// without core importing an upper layer (or a package) directly. No dedup — every contribution is a
|
|
102
|
-
// distinct module (see `collectManifestFiles`'s own TSDoc).
|
|
103
|
-
nitro.virtual['#kestrel/module-manifests'] = () =>
|
|
104
|
-
renderPackageConcatRegistry({
|
|
105
|
-
packages: PACKAGE_MANIFESTS.map(resolvePackageEntry),
|
|
106
|
-
property: 'manifest',
|
|
107
|
-
consumerFiles: collectManifestFiles(roots),
|
|
108
|
-
preamble: importFieldTypesVirtual,
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
},
|
|
112
|
-
})
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kestrel's own packages that ship discoverable content, by category — a package declares WHAT it
|
|
3
|
-
* contributes via its own `kestrelDiscovery` named export (see `@michaelthielemann/kestrel-core`'s `KestrelPackageDiscovery`
|
|
4
|
-
* TSDoc); this module still needs an explicit list of WHICH packages to check, since a Nitro virtual's
|
|
5
|
-
* generated source is plain text written before anything runs — there is no filesystem/`node_modules` scan
|
|
6
|
-
* to discover the list itself. Deliberately per-category, not one flat package list: a virtual only ever
|
|
7
|
-
* imports a package here if that package actually contributes to ITS category, so (for example) building
|
|
8
|
-
* the schema-tables virtual never eagerly touches `@michaelthielemann/kestrel-collections`, which has none.
|
|
9
|
-
*
|
|
10
|
-
* A package with content in a new category is added to the matching list below when that lands — an
|
|
11
|
-
* explicit, reviewable registration, not a guess. `test/architecture/kestrel-discovery.test.ts`'s
|
|
12
|
-
* "bidirectional consistency" suite computes, against every real workspace package's actual
|
|
13
|
-
* `kestrelDiscovery` export, that these three lists are exactly right (nothing missing, nothing stale) —
|
|
14
|
-
* so a package that starts or stops contributing a category and forgets to update the matching list here
|
|
15
|
-
* fails a fast unit test, not a runtime 404 discovered later.
|
|
16
|
-
*/
|
|
17
|
-
export const PACKAGE_COLLECTIONS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-collections', '@michaelthielemann/kestrel-publishing']
|
|
18
|
-
export const PACKAGE_SCHEMA_TABLES = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']
|
|
19
|
-
export const PACKAGE_MANIFESTS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']
|