@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,273 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
-
import { join, sep } from 'node:path'
|
|
3
|
-
|
|
4
|
-
const PLUGIN_EXTENSIONS = ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts', 'tsx', 'jsx']
|
|
5
|
-
|
|
6
|
-
/** One of Kestrel's own `server/plugins/**` files, in the position it must execute at. */
|
|
7
|
-
export interface PluginEntry {
|
|
8
|
-
/** The layer directory name (`layers/<layer>`). */
|
|
9
|
-
layer: string
|
|
10
|
-
/** Path relative to the layer root. */
|
|
11
|
-
file: string
|
|
12
|
-
/** Keys (`${layer}/${file}`) of OTHER entries that MUST appear at a lower index than this one — empty
|
|
13
|
-
* when nothing's correctness depends on this entry's position. Machine-checked by `validatePluginOrder`
|
|
14
|
-
* against `PLUGIN_ORDER`'s actual array positions, not merely asserted. */
|
|
15
|
-
after: string[]
|
|
16
|
-
/** One line: the real dependency this entry's `after` encodes (when non-empty), or the evidence it's
|
|
17
|
-
* safe to reorder (when empty) — always named, never assumed. */
|
|
18
|
-
evidence: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** `${layer}/${file}` — the identity `after` references and `validatePluginOrder` indexes by. */
|
|
22
|
-
const key = (p: Pick<PluginEntry, 'layer' | 'file'>): string => `${p.layer}/${p.file}`
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The declared execution order for every plugin Kestrel itself ships — DATA, read and pushed by
|
|
26
|
-
* `layers/core/modules/plugin-order/index.ts`, replacing Nitro's own layer-then-filename sort for these
|
|
27
|
-
* files. `validatePluginOrder` (below) makes TWO kinds of drift a loud build failure instead of a silent
|
|
28
|
-
* Nitro auto-append or an unenforced comment: (1) a plugin file on disk that isn't declared here (or a
|
|
29
|
-
* declared entry with no file behind it), and (2) an `after` dependency whose real position in this array
|
|
30
|
-
* doesn't actually satisfy it — a reshuffle that moves a dependent entry ahead of what it depends on fails
|
|
31
|
-
* the SAME way a missing/phantom file does, not silently.
|
|
32
|
-
*
|
|
33
|
-
* Audited against a real boot in both contexts (in-repo `nuxt build` and the playground's
|
|
34
|
-
* `extends: ['..', ...]` composition) via a throwaway probe module reading the resolved
|
|
35
|
-
* `nitro.options.plugins`. Confirmed identical for Kestrel's own 6 layers in both contexts:
|
|
36
|
-
* core → fields → media → auth → collections → public,
|
|
37
|
-
* filename-sort within each layer — which is exactly what this list encodes explicitly.
|
|
38
|
-
*/
|
|
39
|
-
export const PLUGIN_ORDER: PluginEntry[] = [
|
|
40
|
-
{
|
|
41
|
-
layer: 'core', file: 'server/plugins/00.config.ts', after: [],
|
|
42
|
-
evidence: 'Nothing depends on core/00.config.ts to come after anything else; everything downstream depends on IT (see the after: entries below).',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
layer: 'core', file: 'server/plugins/00.migrate.ts', after: ['core/server/plugins/00.config.ts'],
|
|
46
|
-
evidence: 'Calls useDb(), which reads the config provider 00.config.ts seeds.',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
layer: 'core', file: 'server/plugins/01.register-introspection-pipeline.ts', after: [],
|
|
50
|
-
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
layer: 'core', file: 'server/plugins/01.register-openapi-pipeline.ts', after: [],
|
|
54
|
-
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
layer: 'core', file: 'server/plugins/01.register-tooling-pipelines.ts', after: [],
|
|
58
|
-
evidence: 'registerPipeline into a Map; read lazily on first request, never at another plugin\'s init.',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
layer: 'core', file: 'server/plugins/02.schema-sync.ts', after: ['core/server/plugins/00.migrate.ts'],
|
|
62
|
-
evidence: 'Dev-only additive auto-migration; its own header comment: "Runs after 00.migrate, so built-in collections are already migrated and this is a no-op on them".',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
layer: 'core', file: 'server/plugins/04.outbox-worker.ts', after: [],
|
|
66
|
-
evidence: 'Starts its own setInterval poll loop against useDb(); no other plugin\'s behavior depends on when this one starts polling (the poll itself is unconditional, no readiness handshake with another plugin).',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
layer: 'core', file: 'server/plugins/05.reindex-refs.ts', after: [],
|
|
70
|
-
evidence: 'registerReindexRefs() registers a handler on the outbox event bus; dispatched later by the outbox worker, never read at another plugin\'s init.',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
layer: 'fields', file: 'server/plugins/01.register-blocks-pipeline.ts', after: [],
|
|
74
|
-
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
layer: 'fields', file: 'server/plugins/01.register-field-populate.ts', after: [],
|
|
78
|
-
evidence: 'Registers the single global row populator; field populators (media/link/richtext/relation) are looked up at READ time, so this plugin\'s order vs. the layer plugins that register them does not matter (stated in the plugin\'s own header comment).',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
layer: 'media', file: 'server/plugins/01.register-media-pipelines.ts', after: [],
|
|
82
|
-
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
layer: 'media', file: 'server/plugins/02.register-media.ts', after: [],
|
|
86
|
-
evidence: 'registerFieldPopulator; looked up at read time, not at another plugin\'s init.',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
layer: 'media', file: 'server/plugins/04.variant-capture.ts', after: [],
|
|
90
|
-
evidence: 'Hooks the render request context (isRendererContext) and stashes discovered variants during SSR; reconciled per-render, no dependency on another plugin having run first.',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
layer: 'media', file: 'server/plugins/05.media-cleanup.ts', after: [],
|
|
94
|
-
evidence: 'registerMediaCleanup() registers a handler on the outbox event bus; dispatched later.',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
layer: 'auth', file: 'server/plugins/01.register-auth-pipelines.ts', after: [],
|
|
98
|
-
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
layer: 'collections', file: 'server/plugins/01.register.ts', after: [],
|
|
102
|
-
evidence: 'Reads the #kestrel/collections and #kestrel/blocks VIRTUALS directly (never allCollections()); writes the runtime registry, but no plugin in this list reads that registry at its own init — only at request time, well after every plugin has finished (documented invariant, docs/internals/architecture.md § Server plugins).',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
layer: 'collections', file: 'server/plugins/02.register-relation-populate.ts', after: [],
|
|
106
|
-
evidence: 'registerFieldPopulator; looked up at read time.',
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
layer: 'public', file: 'server/plugins/00.ensure-snapshot-triggers.ts',
|
|
110
|
-
after: ['core/server/plugins/00.migrate.ts', 'core/server/plugins/02.schema-sync.ts'],
|
|
111
|
-
evidence: 'THE one real cross-layer dependency: needs the snapshot store\'s table to already exist, created by core\'s 00.migrate (in-repo) or 02.schema-sync (consumer). Self-tolerant even so — a boot before either has run logs a warning and returns rather than crashing (ensureSnapshotTriggers\'s own TSDoc); still placed after both so the warning path is never hit in practice.',
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
layer: 'public', file: 'server/plugins/01.register-public-pipelines.ts', after: [],
|
|
115
|
-
evidence: 'registerPipeline into a Map; read lazily on first request.',
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
layer: 'public', file: 'server/plugins/02.register-links.ts', after: [],
|
|
119
|
-
evidence: 'registerFieldPopulator; looked up at read time.',
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
layer: 'public', file: 'server/plugins/03.redirects.ts', after: [],
|
|
123
|
-
evidence: 'registerAfterStep registers a DEFERRED, when-scoped pipeline hook; invoked on a future write, not at another plugin\'s init.',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
layer: 'public', file: 'server/plugins/05.plan-publish.ts', after: [],
|
|
127
|
-
evidence: 'Registers planPublish on the outbox bus; dispatched later by the outbox worker, after commit — never at plugin init.',
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
layer: 'public', file: 'server/plugins/zz.publish.ts', after: [],
|
|
131
|
-
evidence: 'Registers a deferred CRITICAL after-step and starts the boot publish; the boot publish\'s own registry read sits after an `await` inside publishFull, so it resumes only once the synchronous plugin loop (including collections\' 01.register) has already finished — order-free by construction (documented invariant, docs/internals/architecture.md § Server plugins).',
|
|
132
|
-
},
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
/** POSIX-normalize a path for comparison — `path.join`'s OUTPUT is OS-native (backslash on Windows), but
|
|
136
|
-
* `PluginEntry.file` is authored with literal forward slashes (`'server/plugins/00.config.ts'`) and Nuxt's
|
|
137
|
-
* own `_layers[].cwd` values are not guaranteed to be OS-native either (bundlers commonly normalize to
|
|
138
|
-
* POSIX internally regardless of platform). Comparing through one canonical POSIX form — rather than two
|
|
139
|
-
* parallel branches guessing which separator a given string happens to use — removes the asymmetry a
|
|
140
|
-
* Windows build would otherwise hit. */
|
|
141
|
-
const toPosix = (p: string): string => p.split(sep).join('/')
|
|
142
|
-
|
|
143
|
-
/** Find layer `name`'s root among Nuxt's resolved `_layers` cwds. Nitro plugin paths must be absolute
|
|
144
|
-
* filesystem paths — there is no bare-specifier equivalent for a Nitro plugin the way there is for a
|
|
145
|
-
* package export — so this lookup (mirroring the pattern auto-discovery used for the pre-package fields
|
|
146
|
-
* seed) is still needed. */
|
|
147
|
-
function layerRoot(roots: string[], name: string): string {
|
|
148
|
-
const root = roots.find((r) => toPosix(r).endsWith(`/layers/${name}`))
|
|
149
|
-
if (!root) throw new Error(`[kestrel] plugin-order: could not locate the "${name}" layer root`)
|
|
150
|
-
return root
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** `PLUGIN_ORDER` (or an injected `list`, for testing drift scenarios without editing the real constant),
|
|
154
|
-
* resolved to absolute file paths in declared order — what gets pushed into `nitro.options.plugins`. */
|
|
155
|
-
export function resolvePluginOrder(roots: string[], list: PluginEntry[] = PLUGIN_ORDER): string[] {
|
|
156
|
-
return list.map((p) => join(layerRoot(roots, p.layer), p.file))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Nitro's OWN plugin file extension set (`nitropack`'s `GLOB_SCAN_PATTERN` for `plugins/`) — matched
|
|
160
|
-
* exactly, not approximated, so this scan can never be fooled by a file Nitro would pick up that this
|
|
161
|
-
* didn't look for. */
|
|
162
|
-
const PLUGIN_FILE_RE = new RegExp(`\\.(${PLUGIN_EXTENSIONS.join('|')})$`)
|
|
163
|
-
const isPluginFile = (name: string) => PLUGIN_FILE_RE.test(name) && !name.endsWith('.test.ts') && !/\.test\.(js|mjs|cjs|mts|cts|tsx|jsx)$/.test(name)
|
|
164
|
-
|
|
165
|
-
/** Every file under `dir` Nitro's own scan would find — RECURSIVELY (Nitro's glob is `plugins/**\/*.{ext}`,
|
|
166
|
-
* not a flat listing), matching Nitro's exact extension set. Mirrors Nitro's own convention for real, not
|
|
167
|
-
* approximately. */
|
|
168
|
-
function walkPluginFiles(dir: string, out: string[] = []): string[] {
|
|
169
|
-
if (!existsSync(dir)) return out
|
|
170
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
171
|
-
const full = join(dir, entry.name)
|
|
172
|
-
if (entry.isDirectory()) walkPluginFiles(full, out)
|
|
173
|
-
else if (isPluginFile(entry.name)) out.push(full)
|
|
174
|
-
}
|
|
175
|
-
return out
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Every plugin file Nitro would ACTUALLY scan across the layers `list` declares — derived from `list`
|
|
179
|
-
* itself (not a separately hardcoded layer set) so an injected test `list` governs exactly the layers it
|
|
180
|
-
* claims to, never silently falling back to a hardcoded layer set regardless of what was actually passed
|
|
181
|
-
* in — the shape a test injecting a fully isolated fake `list`/`roots` pair depends on. */
|
|
182
|
-
function scanActualPlugins(roots: string[], list: PluginEntry[]): string[] {
|
|
183
|
-
const layers = [...new Set(list.map((p) => p.layer))]
|
|
184
|
-
const found: string[] = []
|
|
185
|
-
for (const layer of layers) found.push(...walkPluginFiles(join(layerRoot(roots, layer), 'server/plugins')))
|
|
186
|
-
return found.sort()
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** `PLUGIN_ORDER` entries reachable from `list`, by `key`. */
|
|
190
|
-
function assertOrderConstraintsSatisfied(list: PluginEntry[]): void {
|
|
191
|
-
const indexOf = new Map(list.map((p, i) => [key(p), i]))
|
|
192
|
-
const violations: string[] = []
|
|
193
|
-
for (const p of list) {
|
|
194
|
-
for (const dep of p.after) {
|
|
195
|
-
const depIndex = indexOf.get(dep)
|
|
196
|
-
const ownIndex = indexOf.get(key(p))!
|
|
197
|
-
if (depIndex === undefined) {
|
|
198
|
-
violations.push(`${key(p)} declares after: "${dep}", but no such entry exists in PLUGIN_ORDER`)
|
|
199
|
-
} else if (depIndex >= ownIndex) {
|
|
200
|
-
violations.push(`${key(p)} must come after ${dep} (${p.evidence}), but is declared at index ${ownIndex} vs. ${dep}'s index ${depIndex}`)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (violations.length) {
|
|
205
|
-
throw new Error(['[kestrel] plugin-order: PLUGIN_ORDER violates a declared after: dependency.', ...violations.map((v) => ` ${v}`)].join('\n'))
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* LOUD failure for BOTH kinds of drift: (1) a plugin file present on disk but missing from `PLUGIN_ORDER`
|
|
211
|
-
* (which Nitro's own scan-and-append would otherwise silently tack onto the END, in undeclared position —
|
|
212
|
-
* see nitropack's `scanPlugins`/`options.plugins.push` for exactly that fallback behavior) or a declared
|
|
213
|
-
* entry whose file no longer exists; (2) a declared `after` dependency the array's ACTUAL positions don't
|
|
214
|
-
* satisfy — a reshuffle, not just a deletion, fails just as loudly. Throws — the ONLY way a Nuxt module's
|
|
215
|
-
* `setup()` can fail the build loudly (a Nitro hook can't itself abort configuration once already running).
|
|
216
|
-
* Called from `setup()`, before the `nitro:config` hook — a build-time failure, not a Nitro-time one.
|
|
217
|
-
*/
|
|
218
|
-
export function validatePluginOrder(roots: string[], list: PluginEntry[] = PLUGIN_ORDER): void {
|
|
219
|
-
assertOrderConstraintsSatisfied(list)
|
|
220
|
-
|
|
221
|
-
const declared = new Set(resolvePluginOrder(roots, list))
|
|
222
|
-
const actual = new Set(scanActualPlugins(roots, list))
|
|
223
|
-
const missing = [...actual].filter((f) => !declared.has(f)) // on disk, not declared
|
|
224
|
-
const phantom = [...declared].filter((f) => !actual.has(f)) // declared, not on disk
|
|
225
|
-
if (missing.length === 0 && phantom.length === 0) return
|
|
226
|
-
const lines: string[] = ['[kestrel] plugin-order: PLUGIN_ORDER has drifted from the real server/plugins/ files.']
|
|
227
|
-
if (missing.length) lines.push(` On disk but NOT declared (would silently land last, undeclared, via Nitro's own scan-and-append): ${missing.join(', ')}`)
|
|
228
|
-
if (phantom.length) lines.push(` Declared but the file does not exist: ${phantom.join(', ')}`)
|
|
229
|
-
lines.push(' Fix PLUGIN_ORDER in layers/core/modules/plugin-order/plugin-order.ts.')
|
|
230
|
-
throw new Error(lines.join('\n'))
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The permanent, in-process observer of the REAL merge: `validatePluginOrder`/`resolvePluginOrder` only
|
|
235
|
-
* prove what THIS module pushes — they cannot see what Nitro's own scan-and-append, another module's
|
|
236
|
-
* `nitro:config` hook, or Nitro's own internal/dev-tooling plugins do to the array afterward. Called from
|
|
237
|
-
* `index.ts`'s `nitro:init` hook (which receives the fully-built Nitro instance, after every `nitro:config`
|
|
238
|
-
* hook AND Nitro's own scan have both already run) against the REAL `nitro.options.plugins`, this asserts
|
|
239
|
-
* our declared order survived that whole pipeline intact: every `expected` path appears, in order, as ONE
|
|
240
|
-
* CONTIGUOUS RUN somewhere in the real array — NOT necessarily a prefix. A real `nuxt dev` boot pushes
|
|
241
|
-
* `@nuxt/devtools`'/`@nuxt/nitro-server`'s own internal plugins (an inline devtools runtime, dev-server
|
|
242
|
-
* request logging) ahead of everything via their own earlier-registered `nitro:config` hooks — legitimate
|
|
243
|
-
* Nuxt/Nitro tooling this module neither owns nor should reject, so "prefix" is the wrong bar; "our own
|
|
244
|
-
* declared plugins never get split apart or interleaved with anyone else's" is the real, checkable
|
|
245
|
-
* invariant, and is what this asserts. Also asserts the real array has no duplicate entries anywhere
|
|
246
|
-
* (Nitro's own `!plugins.includes(x)` scan guard is SUPPOSED to prevent one, but this checks the outcome
|
|
247
|
-
* directly rather than trusting that guard blind). Throws — a boot-time, not merely a config-time, failure.
|
|
248
|
-
*/
|
|
249
|
-
export function assertEffectiveOrderMatches(actualPlugins: (string | undefined)[], expected: string[]): void {
|
|
250
|
-
const cleaned = actualPlugins.filter((p): p is string => typeof p === 'string')
|
|
251
|
-
const seen = new Set<string>()
|
|
252
|
-
const duplicates = new Set<string>()
|
|
253
|
-
for (const p of cleaned) {
|
|
254
|
-
if (seen.has(p)) duplicates.add(p)
|
|
255
|
-
seen.add(p)
|
|
256
|
-
}
|
|
257
|
-
if (duplicates.size) {
|
|
258
|
-
throw new Error(`[kestrel] plugin-order: nitro.options.plugins contains duplicate entries: ${[...duplicates].join(', ')}`)
|
|
259
|
-
}
|
|
260
|
-
if (expected.length === 0) return
|
|
261
|
-
let start = -1
|
|
262
|
-
for (let i = 0; i + expected.length <= cleaned.length; i++) {
|
|
263
|
-
if (expected.every((e, j) => cleaned[i + j] === e)) { start = i; break }
|
|
264
|
-
}
|
|
265
|
-
if (start === -1) {
|
|
266
|
-
throw new Error(
|
|
267
|
-
'[kestrel] plugin-order: the REAL nitro.options.plugins does not contain PLUGIN_ORDER as one unbroken, '
|
|
268
|
-
+ "in-order run — something split our declared block apart or interleaved into it.\n"
|
|
269
|
-
+ ` Expected (contiguous, any position): ${expected.join(', ')}\n`
|
|
270
|
-
+ ` Actual full list: ${cleaned.join(', ')}`,
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url'
|
|
2
|
-
|
|
3
|
-
export default defineNuxtConfig({
|
|
4
|
-
modules: [
|
|
5
|
-
fileURLToPath(new URL('./modules/component-namespace/index.ts', import.meta.url)),
|
|
6
|
-
fileURLToPath(new URL('./modules/auto-discovery/index.ts', import.meta.url)),
|
|
7
|
-
fileURLToPath(new URL('./modules/kestrel/index.ts', import.meta.url)),
|
|
8
|
-
fileURLToPath(new URL('./modules/plugin-order/index.ts', import.meta.url)),
|
|
9
|
-
],
|
|
10
|
-
})
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { getRequestHeader } from 'h3'
|
|
2
|
-
import {
|
|
3
|
-
MAX_JSON_BODY, assertBodyLimit, isRoutablePipeline, toHttpError, tryResolveDefaultPipeline,
|
|
4
|
-
parseFilter, parsePipelineRoute, getCollectionOr404, useDb, readIfUnmodifiedSince,
|
|
5
|
-
} from '@michaelthielemann/kestrel-core'
|
|
6
|
-
import type { PipelineContext, PipelineTrace } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
import { runPipelineForEventAuto, resolveEventPrincipal } from '@michaelthielemann/kestrel-access'
|
|
8
|
-
// The one handler behind the whole pipeline URL scheme:
|
|
9
|
-
//
|
|
10
|
-
// GET /api/<collection>/<readPipeline>[/<id>]
|
|
11
|
-
// POST /api/<collection>/<writePipeline>[/<id>]
|
|
12
|
-
// POST /api/<pipeline> (collection-less: login, publish, createPreview)
|
|
13
|
-
//
|
|
14
|
-
// It is the ONLY route file under server/api: every endpoint is a pipeline, and this is where each one is
|
|
15
|
-
// resolved, verb-checked and run.
|
|
16
|
-
|
|
17
|
-
/** Reads take their whole input from the query string, in the shape the read steps expect. One builder for
|
|
18
|
-
* every read pipeline — the generic list/detail reads and the editor's tooling reads pick the keys they
|
|
19
|
-
* need out of the same object. */
|
|
20
|
-
function readInput(query: Record<string, unknown>): Record<string, unknown> {
|
|
21
|
-
return {
|
|
22
|
-
...query,
|
|
23
|
-
locale: query.locale as string | undefined,
|
|
24
|
-
sort: query.sort as string | undefined,
|
|
25
|
-
page: query.page ? Number(query.page) : undefined,
|
|
26
|
-
perPage: query.perPage ? Number(query.perPage) : undefined,
|
|
27
|
-
filter: parseFilter(query),
|
|
28
|
-
depth: query.depth ? Number(query.depth) : 0,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function assertMethod(method: string, read: boolean, pipeline: string): void {
|
|
33
|
-
const allowed = read ? method === 'GET' || method === 'HEAD' : method === 'POST'
|
|
34
|
-
if (!allowed) {
|
|
35
|
-
throw createError({ statusCode: 405, statusMessage: `${pipeline} is a ${read ? 'read' : 'write'} pipeline — use ${read ? 'GET' : 'POST'}` })
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Embed the trace as a `$`-prefixed sidecar, matching the `$media`/`$translations` convention on rows. A
|
|
40
|
-
* batch/array result (createMany, duplicate) or a bare value (a not-found singleton's `null`) has no
|
|
41
|
-
* object to spread into, so it is wrapped under `data` instead. */
|
|
42
|
-
function embedTrace(result: unknown, trace: PipelineTrace): unknown {
|
|
43
|
-
if (result === null || typeof result !== 'object' || Array.isArray(result)) return { data: result, $pipeline: trace }
|
|
44
|
-
return { ...(result as Record<string, unknown>), $pipeline: trace }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function logPipelineRun(trace: PipelineTrace): void {
|
|
48
|
-
const steps = trace.steps.map((s) => `${s.name}${s.status === 'ok' ? '' : `:${s.status}`}=${s.ms}ms`).join(' ')
|
|
49
|
-
const pipeline = trace.collection ? `${trace.collection}/${trace.pipeline}` : trace.pipeline
|
|
50
|
-
console.log(`[kestrel] pipeline ${pipeline} ${steps} total=${trace.ms}ms`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export default defineEventHandler(async (event) => {
|
|
54
|
-
const route = parsePipelineRoute(event.path)
|
|
55
|
-
const collection = route.collection === null ? null : getCollectionOr404(route.collection)
|
|
56
|
-
const resolved = tryResolveDefaultPipeline(route.collection, route.pipeline)
|
|
57
|
-
if (!isRoutablePipeline(resolved)) {
|
|
58
|
-
throw createError({ statusCode: 404, statusMessage: `Unknown pipeline: ${route.pipeline}` })
|
|
59
|
-
}
|
|
60
|
-
assertMethod(getMethod(event), resolved.read, route.pipeline)
|
|
61
|
-
|
|
62
|
-
const query = getQuery(event) as Record<string, unknown>
|
|
63
|
-
// The body is buffered HERE, before the pipeline's gates run — so it must be bounded, or an anonymous /
|
|
64
|
-
// cross-site / IP-blocked caller could stream an unbounded body into memory before being refused.
|
|
65
|
-
if (!resolved.read && !resolved.rawBody) assertBodyLimit(getRequestHeader(event, 'content-length'), MAX_JSON_BODY)
|
|
66
|
-
let ctx: PipelineContext | undefined
|
|
67
|
-
let result: unknown
|
|
68
|
-
try {
|
|
69
|
-
result = await runPipelineForEventAuto(event, {
|
|
70
|
-
op: route.pipeline,
|
|
71
|
-
collection,
|
|
72
|
-
db: collection ? useDb() : null,
|
|
73
|
-
id: route.id,
|
|
74
|
-
locale: query.locale as string | undefined,
|
|
75
|
-
// A `rawBody` pipeline owns its own body read (multipart, ciphertext, a size-capped stream), so the
|
|
76
|
-
// router must not consume the stream first.
|
|
77
|
-
input: resolved.read ? readInput(query) : resolved.rawBody ? undefined : await readBody(event),
|
|
78
|
-
// Optimistic concurrency: the editor sends the baseline it loaded, and a stale save is refused with
|
|
79
|
-
// 409. Only `updateOne` has a step that reads it.
|
|
80
|
-
work: resolved.read ? {} : { expectedUpdatedAt: readIfUnmodifiedSince(event) },
|
|
81
|
-
onContext: (c) => { ctx = c },
|
|
82
|
-
})
|
|
83
|
-
} catch (error) {
|
|
84
|
-
// The ONE call site that turns a step-raised KestrelError into an HTTP response (see
|
|
85
|
-
// utils/kestrel-error-map.ts); everything else (the runner, the drivers) passes it through untouched.
|
|
86
|
-
// Gate denials (401/403) never reach here as a KestrelError — they're already createError-shaped by
|
|
87
|
-
// the time they leave the runner (see runner.ts's denied/refused) and pass through unchanged.
|
|
88
|
-
throw toHttpError(error)
|
|
89
|
-
} finally {
|
|
90
|
-
if (import.meta.dev && ctx) logPipelineRun(ctx.trace.toJSON())
|
|
91
|
-
}
|
|
92
|
-
if (route.pipeline === 'createOne' || route.pipeline === 'createMany') setResponseStatus(event, 201)
|
|
93
|
-
|
|
94
|
-
if (ctx && query.debug === 'pipeline' && resolveEventPrincipal(event).role === 'admin') {
|
|
95
|
-
return embedTrace(result, ctx.trace.toJSON())
|
|
96
|
-
}
|
|
97
|
-
return result
|
|
98
|
-
})
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { setResolvedKestrelConfig } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { resolveServerKestrelConfig } from '../utils/server-config'
|
|
3
|
-
|
|
4
|
-
// Runs before every other core plugin (00.migrate.ts included — it calls useDb(), which now reads the
|
|
5
|
-
// provider) and before media/auth/collections/public's own plugins (docs/internals/architecture.md § Server
|
|
6
|
-
// plugins: core → media → auth → collections → public). Resolves the config ONCE with the app's own
|
|
7
|
-
// runtimeConfig-preferred, file/env-fallback precedence and pushes it into the config provider — the seam
|
|
8
|
-
// package code (locale.ts, revision-retention.ts, db.ts today; the future package-side movers of the
|
|
9
|
-
// same files) reads from instead of touching `useRuntimeConfig()` directly.
|
|
10
|
-
export default defineNitroPlugin(() => {
|
|
11
|
-
setResolvedKestrelConfig(resolveServerKestrelConfig())
|
|
12
|
-
})
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
|
4
|
-
import { useDb } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
|
|
6
|
-
export default defineNitroPlugin(() => {
|
|
7
|
-
// The committed drizzle-kit migrations only exist in Kestrel's own repo. When Kestrel is consumed as
|
|
8
|
-
// a layer, this folder is absent — there the schema-sync engine (02.schema-sync in dev, the db:migrate
|
|
9
|
-
// task in prod) owns the schema instead (ADR-0002), so skip rather than throw.
|
|
10
|
-
const migrationsFolder = resolve(process.cwd(), 'server/database/migrations')
|
|
11
|
-
if (existsSync(migrationsFolder)) migrate(useDb(), { migrationsFolder })
|
|
12
|
-
})
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { buildIntrospectionPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
-
export default defineNitroPlugin(() => {
|
|
4
|
-
for (const def of buildIntrospectionPipelines()) registerPipeline(def)
|
|
5
|
-
})
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { buildOpenApiPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
-
export default defineNitroPlugin(() => {
|
|
4
|
-
for (const def of buildOpenApiPipelines()) registerPipeline(def)
|
|
5
|
-
})
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { buildToolingPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
-
export default defineNitroPlugin(() => {
|
|
4
|
-
for (const def of buildToolingPipelines()) registerPipeline(def)
|
|
5
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import collections from '#kestrel/collections'
|
|
2
|
-
import schemaTables from '#kestrel/schema-tables'
|
|
3
|
-
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
4
|
-
import { describeOp, desiredFromCollections, isDestructive, planOps, runDevSchemaSync, sqlite, useDb } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
6
|
-
import { resolveServerKestrel, serverRuntimeConfig } from '../utils/server-config'
|
|
7
|
-
// Dev-only schema auto-sync (ADR-0002). Runs after 00.migrate, so built-in collections are already
|
|
8
|
-
// migrated and this is a no-op on them; it additively reconciles the live DB to whatever the registered
|
|
9
|
-
// collections (+ the discovered standalone tables) now compile to — so adding a collection or a field
|
|
10
|
-
// shows up without a hand-written migration. NEVER auto-DDLs in production or during prerender (`nuxt
|
|
11
|
-
// generate`), where schema changes go through committed migrations / an explicit migrate step instead —
|
|
12
|
-
// but production DOES a read-only DRIFT CHECK and warns loudly, so an upgraded consumer that forgot to
|
|
13
|
-
// run `db:migrate` sees a clear boot warning instead of scattered runtime errors.
|
|
14
|
-
export default defineNitroPlugin(() => {
|
|
15
|
-
if (import.meta.prerender) return
|
|
16
|
-
|
|
17
|
-
const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
|
|
18
|
-
// Same toggle source as the registry plugin (01.register), so a disabled built-in is dropped from
|
|
19
|
-
// the desired schema exactly as it is dropped from the runtime surface.
|
|
20
|
-
toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
|
|
21
|
-
extraTables: schemaTables as AnySQLiteTable[],
|
|
22
|
-
})
|
|
23
|
-
const client = (useDb() as unknown as { $client: SyncDb }).$client
|
|
24
|
-
|
|
25
|
-
if (process.env.NODE_ENV === 'production') {
|
|
26
|
-
// Read-only: never DDL in prod. Detect + surface drift so the operator runs db:migrate. Split by kind:
|
|
27
|
-
// ADDITIVE gaps (missing table/column/index) can make the app error → a real "run db:migrate" warning.
|
|
28
|
-
// DESTRUCTIVE ops (a drop for a disabled built-in, or an unmanaged extra table) are NOT app-breaking and
|
|
29
|
-
// are withheld by db:migrate anyway → report them informationally so a permanent, unactionable
|
|
30
|
-
// "app may error" warning doesn't fire on every boot after the operator has done what it asked.
|
|
31
|
-
try {
|
|
32
|
-
const pending = planOps(client, desired)
|
|
33
|
-
const additive = pending.filter((op) => !isDestructive(op))
|
|
34
|
-
const destructive = pending.filter(isDestructive)
|
|
35
|
-
if (additive.length) {
|
|
36
|
-
console.warn(
|
|
37
|
-
`[kestrel] SCHEMA DRIFT: the live DB is missing ${additive.length} change(s) the collections expect — ` +
|
|
38
|
-
`the app may error until you run the db:migrate task. Pending:\n - ${additive.map(describeOp).join('\n - ')}`,
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
if (destructive.length) {
|
|
42
|
-
console.info(
|
|
43
|
-
`[kestrel] schema note: ${destructive.length} table(s)/column(s) exist that the current collections no longer ` +
|
|
44
|
-
`define (e.g. a disabled built-in or an unmanaged table). Not app-breaking; db:migrate withholds these unless ` +
|
|
45
|
-
`you opt in ({force} / {drop:[…]}):\n - ${destructive.map(describeOp).join('\n - ')}`,
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn('[kestrel] could not check schema drift at boot:', (error as Error)?.message ?? error)
|
|
50
|
-
}
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// SQLite is the only backend today; this is the seam where a config-driven `resolveDialect()` would go.
|
|
55
|
-
// runDevSchemaSync isolates failures (logs loudly, keeps the server up) — an infeasible auto-sync must
|
|
56
|
-
// not 500 every route, including the admin UI needed to fix it.
|
|
57
|
-
runDevSchemaSync(client, desired, sqlite)
|
|
58
|
-
})
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { buildOutboxPipelines, makeTicker, registerPipeline, useDb } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
/** Hard-coded to `content` — the only module with an outbox table today; extend alongside
|
|
3
|
-
* `pipelines/outbox.ts` if a second module gets one. */
|
|
4
|
-
const OUTBOX_MODULE = 'content'
|
|
5
|
-
const POLL_INTERVAL_MS = 1000
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The outbox worker: NOT a Nitro `scheduledTask` (those run per-request-worker in a way that doesn't suit
|
|
9
|
-
* an always-on background loop here) — a plain `setInterval` loop started at plugin init, `unref`'d so it
|
|
10
|
-
* never keeps the process alive on its own (a test/dev server can still exit while it's running).
|
|
11
|
-
*
|
|
12
|
-
* Polls every tick, unconditionally — no `PRAGMA data_version` short-circuit: `useDb()` is the process-
|
|
13
|
-
* wide singleton connection every write also goes through, and a connection never observes its OWN writes
|
|
14
|
-
* bumping `data_version` (only a write from a DIFFERENT connection does), so gating on it here would go
|
|
15
|
-
* permanently blind after the first write this same connection ever made. The poll query itself is a
|
|
16
|
-
* covered index search (see `outbox.ts`'s `readPendingOutbox`), cheap enough at a 1s interval to not need
|
|
17
|
-
* a short-circuit at all.
|
|
18
|
-
*/
|
|
19
|
-
export default defineNitroPlugin(() => {
|
|
20
|
-
// `makeTicker` takes `useDb` itself (a thunk), not its result — nothing here calls it, so the db is
|
|
21
|
-
// still untouched at plugin-init time; the interval callback is the first thing that ever does.
|
|
22
|
-
const tick = makeTicker(useDb, OUTBOX_MODULE)
|
|
23
|
-
|
|
24
|
-
// Registration only — nothing here resolves a pipeline or reads the collection registry at init.
|
|
25
|
-
for (const def of buildOutboxPipelines()) registerPipeline(def)
|
|
26
|
-
|
|
27
|
-
const timer = setInterval(() => {
|
|
28
|
-
tick().catch((error: unknown) => console.error('[kestrel] outbox worker tick failed', error))
|
|
29
|
-
}, POLL_INTERVAL_MS)
|
|
30
|
-
timer.unref?.()
|
|
31
|
-
})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { allCollections, buildPipelineIndex, renderDashboard } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { PLUGIN_ORDER } from '../../../modules/plugin-order/plugin-order'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Dev-only introspection dashboard: gathers live registry state, so it reflects the CONSUMER's own
|
|
6
|
-
* collections/pipelines/overrides — not a static analysis of Kestrel's own source. File-based
|
|
7
|
-
* `server/routes/**` modules are compiled into production builds unconditionally, so the
|
|
8
|
-
* `import.meta.dev` check below is the real guard: Nitro inlines it to `false` in production and
|
|
9
|
-
* dead-code-eliminates everything past it, leaving only the 404.
|
|
10
|
-
*/
|
|
11
|
-
export default defineEventHandler((event) => {
|
|
12
|
-
if (!import.meta.dev) {
|
|
13
|
-
throw createError({ statusCode: 404, statusMessage: 'Not found' })
|
|
14
|
-
}
|
|
15
|
-
const collections = allCollections().map((c) => ({
|
|
16
|
-
name: c.name,
|
|
17
|
-
mode: c.def.mode,
|
|
18
|
-
translatable: Boolean(c.def.translatable),
|
|
19
|
-
fieldCount: Object.keys(c.def.fields).length,
|
|
20
|
-
}))
|
|
21
|
-
const html = renderDashboard({
|
|
22
|
-
pipelines: buildPipelineIndex(),
|
|
23
|
-
pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
|
|
24
|
-
collections,
|
|
25
|
-
generatedAt: new Date().toISOString(),
|
|
26
|
-
})
|
|
27
|
-
setHeader(event, 'content-type', 'text/html; charset=utf-8')
|
|
28
|
-
return html
|
|
29
|
-
})
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import collections from '#kestrel/collections'
|
|
2
|
-
import schemaTables from '#kestrel/schema-tables'
|
|
3
|
-
import moduleManifests from '#kestrel/module-manifests'
|
|
4
|
-
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
5
|
-
import type { OwnershipManifest } from '@michaelthielemann/kestrel-contracts'
|
|
6
|
-
import { buildContentManifest, describeOp, desiredFromCollections, isDestructive, opTable, orderManifests, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
import type { BuiltCollection, CollectionDef, Dialect, SchemaSnapshot, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
8
|
-
import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
|
|
9
|
-
// The per-module counterpart to `db:migrate` (ADR-0012): runs the SAME diff/apply engine, but scoped to
|
|
10
|
-
// one ownership manifest's tables at a time, in a fixed order — `content` (the collection/base schema
|
|
11
|
-
// other modules are conceptually layered on) first, then every discovered module manifest
|
|
12
|
-
// (`module-order.ts`'s `MODULE_MIGRATION_ORDER`), then an `unmanaged` catch-all for any table no
|
|
13
|
-
// manifest yet claims (never silently skipped). Payload/trigger mirror `db:migrate` exactly — {module:
|
|
14
|
-
// "media"}=one module only, otherwise every module in order; {check:true}=dry-run, {force:true}=rebuilds,
|
|
15
|
-
// {drop:[names]}=drop tables. Same dev/prod trigger shape as `db:migrate` (see that file's header).
|
|
16
|
-
type ModulePayload = { module?: string; check?: boolean; force?: boolean; drop?: string[] }
|
|
17
|
-
type ModuleResult =
|
|
18
|
-
| { module: string; check: true; pending: string[]; destructive: string[] }
|
|
19
|
-
| { module: string; applied: string[]; skipped: string[] }
|
|
20
|
-
|
|
21
|
-
function runOne(
|
|
22
|
-
module: string,
|
|
23
|
-
tables: readonly string[],
|
|
24
|
-
client: SyncDb,
|
|
25
|
-
desired: SchemaSnapshot,
|
|
26
|
-
dialect: Dialect,
|
|
27
|
-
p: ModulePayload,
|
|
28
|
-
): ModuleResult {
|
|
29
|
-
if (p.check === true) {
|
|
30
|
-
const ops = planOps(client, desired, dialect).filter((op) => tables.includes(opTable(op)))
|
|
31
|
-
const destructive = ops.filter(isDestructive).map(describeOp)
|
|
32
|
-
return { module, check: true, pending: dialect.render(ops), destructive }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const { applied, skipped } = syncSchema(client, desired, {
|
|
36
|
-
allowDestructive: p.force === true,
|
|
37
|
-
dropTables: Array.isArray(p.drop) ? p.drop : undefined,
|
|
38
|
-
tables,
|
|
39
|
-
}, dialect)
|
|
40
|
-
if (skipped.length) {
|
|
41
|
-
console.warn(`[kestrel] db:migrate-module(${module}) withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
|
|
42
|
-
}
|
|
43
|
-
console.info(`[kestrel] db:migrate-module(${module}) applied ${applied.length} statement(s)`)
|
|
44
|
-
return { module, applied, skipped: skipped.map(describeOp) }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default defineTask<ModuleResult[]>({
|
|
48
|
-
meta: {
|
|
49
|
-
name: 'db:migrate-module',
|
|
50
|
-
description: 'Per-module schema migration, ordered (content → media → publishing → unmanaged). {module:"name"}=one module only, otherwise every module in order. {check}/{force}/{drop} match db:migrate.',
|
|
51
|
-
},
|
|
52
|
-
run({ payload }) {
|
|
53
|
-
const p = (payload ?? {}) as ModulePayload
|
|
54
|
-
const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
|
|
55
|
-
toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
|
|
56
|
-
extraTables: schemaTables as AnySQLiteTable[],
|
|
57
|
-
})
|
|
58
|
-
const client = (useDb() as unknown as { $client: SyncDb }).$client
|
|
59
|
-
const dialect = sqlite
|
|
60
|
-
|
|
61
|
-
const manifests = orderManifests([buildContentManifest(), ...(moduleManifests as OwnershipManifest[])])
|
|
62
|
-
const claimed = new Set(manifests.flatMap((m) => m.tables))
|
|
63
|
-
// The `unmanaged` bucket's table list: every table any pending op touches that no manifest claims —
|
|
64
|
-
// computed up front so both `check` and apply modes scope to the SAME set (rather than an unfiltered
|
|
65
|
-
// rescan, which in apply mode would re-touch already-migrated tables and in check mode would re-list
|
|
66
|
-
// every module's pending ops a second time under `unmanaged`).
|
|
67
|
-
const unmanagedTables = [...new Set(planOps(client, desired, dialect).map(opTable))].filter((t) => !claimed.has(t))
|
|
68
|
-
|
|
69
|
-
if (p.module) {
|
|
70
|
-
const manifest = manifests.find((m) => m.module === p.module)
|
|
71
|
-
const tables = manifest ? manifest.tables : p.module === 'unmanaged' ? unmanagedTables : undefined
|
|
72
|
-
if (!tables) {
|
|
73
|
-
const known = [...manifests.map((m) => m.module), 'unmanaged'].join(', ')
|
|
74
|
-
throw new Error(`kestrel: db:migrate-module: unknown module "${p.module}" — known: ${known}`)
|
|
75
|
-
}
|
|
76
|
-
return { result: [runOne(p.module, tables, client, desired, dialect, p)] }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const results = manifests.map((m) => runOne(m.module, m.tables, client, desired, dialect, p))
|
|
80
|
-
results.push(runOne('unmanaged', unmanagedTables, client, desired, dialect, p))
|
|
81
|
-
return { result: results }
|
|
82
|
-
},
|
|
83
|
-
})
|