@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,37 +0,0 @@
|
|
|
1
|
-
import type Database from 'better-sqlite3'
|
|
2
|
-
import { pagesToRoutes, type PageRouteRow } from './routes'
|
|
3
|
-
|
|
4
|
-
/** Tables the schema engine marked routable, by their partial `path` index (see `collectPageRoutes`).
|
|
5
|
-
* Empty means this file holds no page-like collection at all — a structural fact about the schema, not a
|
|
6
|
-
* statement about how many pages exist: a CMS with one page still has its table and that index. */
|
|
7
|
-
export function pageLikeTables(db: Database.Database): string[] {
|
|
8
|
-
return (db
|
|
9
|
-
.prepare("SELECT DISTINCT tbl_name AS name FROM sqlite_master WHERE type = 'index' AND sql LIKE '%path is not null%'")
|
|
10
|
-
.all() as { name: string }[]).map((t) => t.name)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Page routes to seed for `nuxt generate`, discovered straight from the build-time DB (no registry is
|
|
15
|
-
* available at build). Every page-like collection carries a **partial unique index on `path`**
|
|
16
|
-
* (`… WHERE path is not null`) — the schema engine's marker for a routable table — so we enumerate those.
|
|
17
|
-
* That is reliable where "has a `path` column" is NOT: the media `folders` registry has a `path` column +
|
|
18
|
-
* a plain unique index, but no such partial clause, so it is correctly excluded. Per pageLike table:
|
|
19
|
-
* published rows (only where a `status` column exists) with a non-null path, keyed by their own `locale`
|
|
20
|
-
* (or the primary locale for a non-translatable collection). Returns the deduped, sorted route list
|
|
21
|
-
* (always including the root).
|
|
22
|
-
*/
|
|
23
|
-
export function collectPageRoutes(db: Database.Database, primaryLocale: string, prefixPrimary = false): string[] {
|
|
24
|
-
const rows: PageRouteRow[] = []
|
|
25
|
-
for (const name of pageLikeTables(db)) {
|
|
26
|
-
const q = `"${name.replace(/"/g, '""')}"`
|
|
27
|
-
const cols = (db.prepare(`PRAGMA table_info(${q})`).all() as { name: string }[]).map((c) => c.name)
|
|
28
|
-
const statusClause = cols.includes('status') ? "status = 'published' AND " : ''
|
|
29
|
-
if (cols.includes('locale')) {
|
|
30
|
-
rows.push(...(db.prepare(`SELECT path, locale FROM ${q} WHERE ${statusClause}path IS NOT NULL`).all() as PageRouteRow[]))
|
|
31
|
-
} else {
|
|
32
|
-
const bare = db.prepare(`SELECT path FROM ${q} WHERE ${statusClause}path IS NOT NULL`).all() as { path: string | null }[]
|
|
33
|
-
for (const r of bare) rows.push({ path: r.path, locale: primaryLocale })
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return pagesToRoutes(rows, primaryLocale, prefixPrimary)
|
|
37
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { cpus } from 'node:os'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { defineNuxtModule } from '@nuxt/kit'
|
|
4
|
-
import Database from 'better-sqlite3'
|
|
5
|
-
import { collectPageRoutes, pageLikeTables } from './discover'
|
|
6
|
-
import { localePath, resolveKestrel } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
import type { KestrelConfig } from '@michaelthielemann/kestrel-core'
|
|
8
|
-
import { META_KEYS } from '@michaelthielemann/kestrel-core'
|
|
9
|
-
import { recordRouteDiscovery, type RouteDiscovery } from '@michaelthielemann/kestrel-delivery-static'
|
|
10
|
-
// Read published paths from EVERY page-like collection straight from the DB at build time so
|
|
11
|
-
// `nuxt generate` prerenders one HTML file per page (see `collectPageRoutes` for how pageLike tables
|
|
12
|
-
// are identified). The server's migrate-on-boot hasn't run yet at this point, so a DB that isn't there
|
|
13
|
-
// yet degrades to the root alone — reported as `incomplete`, because a deploy that reconciles must be
|
|
14
|
-
// able to tell that degrade apart from a CMS that genuinely holds one page.
|
|
15
|
-
// `reconciles` (an S3 deploy follows this build) makes an unreadable-but-present DB fail the build
|
|
16
|
-
// instead: there the degraded tree would also be uploaded over a real live site. Without that step there
|
|
17
|
-
// is nothing destructive downstream, so a degrade stays a warning.
|
|
18
|
-
export function discoverRoutes(path: string, primary: string, prefixPrimary: boolean, reconciles = false): RouteDiscovery {
|
|
19
|
-
const root = localePath('/', primary, primary, prefixPrimary) // `/` or `/<primary>` when the primary is prefixed
|
|
20
|
-
if (path === ':memory:') return { routes: [root], incomplete: 'the build ran against an in-memory database — only the site root was prerendered' }
|
|
21
|
-
if (!existsSync(path)) {
|
|
22
|
-
console.warn(`[kestrel] prerender: no database at ${path} — prerendering the site root only`)
|
|
23
|
-
return { routes: [root], incomplete: `no database at ${path} — only the site root was prerendered` }
|
|
24
|
-
}
|
|
25
|
-
let db: Database.Database | undefined
|
|
26
|
-
try {
|
|
27
|
-
db = new Database(path, { readonly: true, fileMustExist: true })
|
|
28
|
-
// Opening the file is not enumerating it: an unmigrated, zero-byte or simply wrong `KESTREL_DB` file
|
|
29
|
-
// opens as a valid EMPTY database, and the root-only route list it yields is byte-identical to a
|
|
30
|
-
// complete enumeration. Report the missing schema so a reconcile downstream can tell the two apart.
|
|
31
|
-
if (!pageLikeTables(db).length) {
|
|
32
|
-
const why = `the database at ${path} holds no page-like table — only the site root was prerendered`
|
|
33
|
-
console.warn(`[kestrel] prerender: ${why}`)
|
|
34
|
-
return { routes: [root], incomplete: why }
|
|
35
|
-
}
|
|
36
|
-
return { routes: collectPageRoutes(db, primary, prefixPrimary) }
|
|
37
|
-
} catch (error) {
|
|
38
|
-
const why = `cannot read the database at ${path}: ${(error as Error)?.message ?? error}`
|
|
39
|
-
if (reconciles) throw new Error(`[kestrel] prerender: ${why} — refusing to ship a site with no pages to an S3 target`)
|
|
40
|
-
console.warn(`[kestrel] prerender: ${why} — prerendering the site root only`)
|
|
41
|
-
return { routes: [root], incomplete: why }
|
|
42
|
-
} finally {
|
|
43
|
-
db?.close()
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default defineNuxtModule({
|
|
48
|
-
meta: { name: 'kestrel-prerender-routes' },
|
|
49
|
-
setup(_options, nuxt) {
|
|
50
|
-
// Build-time: resolve directly from the consumer's config (runtimeConfig/useRuntimeConfig aren't
|
|
51
|
-
// available here), rather than the request-time locale.ts globals.
|
|
52
|
-
const c = resolveKestrel(nuxt.options.kestrel as KestrelConfig, process.env, nuxt.options.rootDir)
|
|
53
|
-
// The editor-preview fallback page must NEVER be prerendered: the classic `nuxt generate` (static
|
|
54
|
-
// preset) auto-seeds every static page path, and this page 404s without an admin session — a
|
|
55
|
-
// guaranteed prerender error on every SSG build. Registered unconditionally (before the output.auto
|
|
56
|
-
// early-return) so both publishing models exclude it.
|
|
57
|
-
nuxt.hook('nitro:config', (nitro) => {
|
|
58
|
-
nitro.prerender ||= {}
|
|
59
|
-
nitro.prerender.ignore = [...(nitro.prerender.ignore ?? []), '/__kestrel/preview']
|
|
60
|
-
})
|
|
61
|
-
// The runtime publisher (output.auto) and build-time prerendering are mutually exclusive: a prerendered
|
|
62
|
-
// `.output/public/<route>/index.html` SHADOWS the live SSR route, so the publisher's localFetch would
|
|
63
|
-
// read STALE build-time HTML instead of rendering fresh. With auto on, the publisher (boot + on-write)
|
|
64
|
-
// owns publishing — so skip prerender seeding entirely. (auto off = the classic `nuxt generate` SSG model.)
|
|
65
|
-
if (c.output.auto) return
|
|
66
|
-
nuxt.hook('nitro:config', (nitro) => {
|
|
67
|
-
// Only an S3 target reconciles (deletes what this build didn't produce); a local build leaves the
|
|
68
|
-
// tree on disk, so an incomplete enumeration destroys nothing there. `nitro.static` is the same flag
|
|
69
|
-
// the deploy itself gates on: this hook also runs for `nuxt dev` and a plain `nuxt build`, and
|
|
70
|
-
// neither ships anything, so there a degrade must stay a warning instead of failing the command.
|
|
71
|
-
const reconciles = c.output.driver === 's3' && nitro.static === true
|
|
72
|
-
const discovery = discoverRoutes(c.dbPath, c.primaryLocale, c.prefixPrimary, reconciles)
|
|
73
|
-
// Hand the deploy module the completeness of THIS enumeration — the only step that knows it.
|
|
74
|
-
recordRouteDiscovery(nuxt, discovery)
|
|
75
|
-
nitro.prerender ||= {}
|
|
76
|
-
// Every meta artifact EXCEPT `llms-full.txt`, which is seeded only when the consumer opted in — the
|
|
77
|
-
// route 404s otherwise, and a prerender error fails the whole `nuxt generate`.
|
|
78
|
-
const meta = META_KEYS.filter((k) => k !== 'llms-full.txt' || c.seo.llmsFull).map((k) => `/${k}`)
|
|
79
|
-
nitro.prerender.routes = [...new Set([...(nitro.prerender.routes ?? []), ...discovery.routes, ...meta])]
|
|
80
|
-
// Render pages in parallel — the dominant lever on `nuxt generate` wall-clock as page count grows.
|
|
81
|
-
// Safe: page reads are synchronous better-sqlite3 (WAL + busy_timeout, can't interleave mid-statement);
|
|
82
|
-
// the one WRITE on this path — the variant-registry capture — is an IMMEDIATE transaction, so
|
|
83
|
-
// concurrent prerender writers (and a co-running server publisher) serialize instead of losing updates.
|
|
84
|
-
// Capped to avoid memory over-subscription; overridable.
|
|
85
|
-
nitro.prerender.concurrency ??= Math.min(Math.max(cpus().length, 2), 8)
|
|
86
|
-
})
|
|
87
|
-
},
|
|
88
|
-
})
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { localePath } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
|
|
3
|
-
export interface PageRouteRow {
|
|
4
|
-
path: string | null
|
|
5
|
-
locale: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function pagesToRoutes(rows: PageRouteRow[], primaryLocale: string, prefixPrimary = false): string[] {
|
|
9
|
-
// The site root is the primary-locale home — `/` normally, `/<primary>` when the primary is prefixed.
|
|
10
|
-
const routes = new Set<string>([localePath('/', primaryLocale, primaryLocale, prefixPrimary)])
|
|
11
|
-
for (const { path, locale } of rows) {
|
|
12
|
-
if (!path) continue
|
|
13
|
-
routes.add(localePath(path, locale, primaryLocale, prefixPrimary))
|
|
14
|
-
}
|
|
15
|
-
return [...routes].sort()
|
|
16
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { defineNuxtModule } from '@nuxt/kit'
|
|
2
|
-
import Database from 'better-sqlite3'
|
|
3
|
-
import { resolveKestrel, type KestrelConfig } from '@michaelthielemann/kestrel-core'
|
|
4
|
-
import { isEnvTrue } from '@michaelthielemann/kestrel-delivery-static'
|
|
5
|
-
import { pruneUnreferencedMedia, mediaOwnedKeys } from './prune-media'
|
|
6
|
-
|
|
7
|
-
// The set of storage keys the media library owns (originals + derivatives), read straight from the DB at
|
|
8
|
-
// build time. Returns null when the registry can't be read (no DB, no media table) — the caller then SKIPS
|
|
9
|
-
// the prune entirely rather than risk deleting extension/consumer blobs (e.g. gallery ciphertext).
|
|
10
|
-
function readOwnedKeys(dbPath: string): Set<string> | null {
|
|
11
|
-
if (dbPath === ':memory:') return null
|
|
12
|
-
try {
|
|
13
|
-
const db = new Database(dbPath, { readonly: true, fileMustExist: true })
|
|
14
|
-
try {
|
|
15
|
-
const rows = db.prepare('SELECT storage_key AS storageKey, derivatives FROM media').all() as { storageKey: string; derivatives: string | null }[]
|
|
16
|
-
return mediaOwnedKeys(rows.map((r) => ({ storageKey: r.storageKey, derivatives: r.derivatives ? JSON.parse(r.derivatives) : null })))
|
|
17
|
-
} finally {
|
|
18
|
-
db.close()
|
|
19
|
-
}
|
|
20
|
-
} catch {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// `nuxt generate` bakes the WHOLE upload dir into .output/public/<baseUrl>; without this, images of
|
|
26
|
-
// deleted/unpublished pages ship to prod. At `compiled` (full output on disk, post prerender-abort) delete
|
|
27
|
-
// every baked media file no generated page references — prunes the BAKE only, so a wrong prune costs a
|
|
28
|
-
// re-generate, not data. Local-driver only (only it registers the /uploads publicAssets that gets baked).
|
|
29
|
-
// If deploy-output is ever added to modules[], it must come AFTER this so the prune runs before the ship.
|
|
30
|
-
export default defineNuxtModule({
|
|
31
|
-
meta: { name: 'kestrel-prune-media' },
|
|
32
|
-
setup(_options, nuxt) {
|
|
33
|
-
const c = resolveKestrel(nuxt.options.kestrel as KestrelConfig, process.env, nuxt.options.rootDir)
|
|
34
|
-
if (c.media.driver !== 'local') return
|
|
35
|
-
const dryRun = isEnvTrue(process.env.KESTREL_OUTPUT_DRY_RUN)
|
|
36
|
-
nuxt.hook('nitro:init', (nitro) => {
|
|
37
|
-
nitro.hooks.hook('compiled', () => {
|
|
38
|
-
if (nitro.options.static !== true) return
|
|
39
|
-
const ownedKeys = readOwnedKeys(c.dbPath)
|
|
40
|
-
if (!ownedKeys) {
|
|
41
|
-
console.log('[kestrel] media prune skipped: media registry unavailable (would risk deleting extension/consumer blobs)')
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
pruneUnreferencedMedia(nitro.options.output.publicDir, c.media.baseUrl, { dryRun, ownedKeys, log: (m) => console.log(`[kestrel] ${m}`) })
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
},
|
|
48
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
2
|
-
import { join, relative, sep } from 'node:path'
|
|
3
|
-
|
|
4
|
-
function escapeRe(s: string): string {
|
|
5
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// Prefix-relative keys referenced in `text`. The charset stops at ?/#/quote/space, so query strings,
|
|
9
|
-
// fragments and srcset descriptors (320w, 2x) fall outside the key; the leading `/` keeps `/uploads` from
|
|
10
|
-
// matching `/myuploads`.
|
|
11
|
-
export function referencedKeys(text: string, prefix: string): Set<string> {
|
|
12
|
-
const out = new Set<string>()
|
|
13
|
-
const re = new RegExp(`${escapeRe(prefix)}/([A-Za-z0-9._/-]+)`, 'g')
|
|
14
|
-
for (const m of text.matchAll(re)) out.add(m[1])
|
|
15
|
-
return out
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// The prunable candidates: unreferenced files that the media library OWNS. When `ownedKeys` is given, a
|
|
19
|
-
// file not in it (gallery ciphertext / index, or any consumer blob written via useStorageDriver) is NEVER
|
|
20
|
-
// a candidate — those are baked deliberately and their URLs are built at runtime, so they never appear as
|
|
21
|
-
// literal keys in the output. Without `ownedKeys` the legacy "prune any unreferenced key" behaviour holds.
|
|
22
|
-
export function planMediaPrune(files: string[], referenced: Set<string>, ownedKeys?: Set<string>): string[] {
|
|
23
|
-
return files.filter((f) => !referenced.has(f) && (!ownedKeys || ownedKeys.has(f)))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// The storage keys the media library owns: every original + every derivative object it tracks. Used to
|
|
27
|
-
// scope the bake prune so it can only ever delete media-library artifacts, never extension/consumer blobs.
|
|
28
|
-
export function mediaOwnedKeys(rows: { storageKey?: string | null; derivatives?: Record<string, { key?: string }> | null }[]): Set<string> {
|
|
29
|
-
const out = new Set<string>()
|
|
30
|
-
for (const r of rows) {
|
|
31
|
-
if (r.storageKey) out.add(r.storageKey)
|
|
32
|
-
for (const e of Object.values(r.derivatives ?? {})) if (e?.key) out.add(e.key)
|
|
33
|
-
}
|
|
34
|
-
return out
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function walkFiles(dir: string): string[] {
|
|
38
|
-
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
39
|
-
.filter((e) => e.isFile())
|
|
40
|
-
.map((e) => join(e.parentPath, e.name))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface PruneResult {
|
|
44
|
-
kept: number
|
|
45
|
-
pruned: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface PruneOptions {
|
|
49
|
-
/** Report what would be deleted without touching the filesystem. */
|
|
50
|
-
dryRun?: boolean
|
|
51
|
-
/** Restrict deletion to these media-library-owned keys; anything else baked under the root is kept. */
|
|
52
|
-
ownedKeys?: Set<string>
|
|
53
|
-
log?: (msg: string) => void
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Delete every baked upload under `publicDir/<baseUrl>` that no generated .html/.css/.json references.
|
|
57
|
-
// Over-scanning those files is keep-on-doubt (a key seen anywhere is kept). No-op when the dir is absent
|
|
58
|
-
// (S3 media / nothing baked). Touches only the bake — the library upload dir is elsewhere.
|
|
59
|
-
export function pruneUnreferencedMedia(publicDir: string, baseUrl: string, opts: PruneOptions = {}): PruneResult {
|
|
60
|
-
const prefix = baseUrl.replace(/\/+$/, '')
|
|
61
|
-
if (!prefix) return { kept: 0, pruned: 0 } // an empty baseUrl would target the whole output — refuse
|
|
62
|
-
const uploadsDir = join(publicDir, prefix)
|
|
63
|
-
if (!existsSync(uploadsDir)) return { kept: 0, pruned: 0 }
|
|
64
|
-
|
|
65
|
-
const referenced = new Set<string>()
|
|
66
|
-
for (const abs of walkFiles(publicDir)) {
|
|
67
|
-
// Also scan JS chunks: a media URL can be emitted only from client JS (a background image, a config
|
|
68
|
-
// object), never as literal HTML/CSS — pruning it would 404 the live reference.
|
|
69
|
-
if (!/\.(?:html|css|json|m?js)$/i.test(abs)) continue
|
|
70
|
-
for (const k of referencedKeys(readFileSync(abs, 'utf8'), prefix)) referenced.add(k)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const files = walkFiles(uploadsDir).map((abs) => ({ abs, key: relative(uploadsDir, abs).split(sep).join('/') }))
|
|
74
|
-
const prune = new Set(planMediaPrune(files.map((f) => f.key), referenced, opts.ownedKeys))
|
|
75
|
-
const toDelete = files.filter((f) => prune.has(f.key))
|
|
76
|
-
if (!opts.dryRun) for (const f of toDelete) rmSync(f.abs, { force: true })
|
|
77
|
-
|
|
78
|
-
const kept = files.length - toDelete.length
|
|
79
|
-
opts.log?.(`media prune${opts.dryRun ? ' (dry-run)' : ''}: kept ${kept}, ${opts.dryRun ? 'would prune' : 'pruned'} ${toDelete.length} unreferenced file(s)`)
|
|
80
|
-
return { kept, pruned: toDelete.length }
|
|
81
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url'
|
|
2
|
-
|
|
3
|
-
import { kestrelComponents } from '../core/modules/component-namespace/shared'
|
|
4
|
-
|
|
5
|
-
export default defineNuxtConfig({
|
|
6
|
-
components: [kestrelComponents(import.meta.url)],
|
|
7
|
-
modules: [
|
|
8
|
-
fileURLToPath(new URL('./modules/prerender-routes/index.ts', import.meta.url)),
|
|
9
|
-
fileURLToPath(new URL('./modules/prune-media/index.ts', import.meta.url)),
|
|
10
|
-
// AFTER prune-media so the bake is pruned before it ships. Self-gates to output.driver:'s3' + a real
|
|
11
|
-
// `nuxt generate`; a no-op otherwise. Without it, output.driver:'s3' + output.auto:false shipped nothing.
|
|
12
|
-
fileURLToPath(new URL('./modules/deploy-output/index.ts', import.meta.url)),
|
|
13
|
-
],
|
|
14
|
-
})
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { getMethod, getRequestHeader, sendRedirect, send, setHeader, setResponseStatus, type H3Event } from 'h3'
|
|
2
|
-
import { isRendererContext } from '@michaelthielemann/kestrel-access'
|
|
3
|
-
import { cacheControlFor, isDeliveryExemptPath, isDeliveryLive } from '@michaelthielemann/kestrel-core'
|
|
4
|
-
import { usePublishingDb, currentSnapshot, htmlKeyForRoute } from '@michaelthielemann/kestrel-publishing'
|
|
5
|
-
import { liveRedirectFor } from '@michaelthielemann/kestrel-delivery-live'
|
|
6
|
-
|
|
7
|
-
// Requests this adapter must never answer, whatever `currentSnapshot` says: the admin app, the pipeline
|
|
8
|
-
// API, Nitro's own internals (tasks — e.g. the manual `publish:run` trigger), built assets, and the
|
|
9
|
-
// editor's preview ticket flow. `META_ROUTES` mirrors `@michaelthielemann/kestrel-core`'s own `META_KEYS`
|
|
10
|
-
// (sitemap/robots/llms/redirects) — those are rendered by their own routes, not snapshots. Every prefix
|
|
11
|
-
// is slash-terminated (`normalizeExemptPrefix`'s own rule): a bare `/admin` would also match `/admin-guide`
|
|
12
|
-
// — a DIFFERENT, publishable route this adapter must still serve from its own snapshot.
|
|
13
|
-
const EXEMPT_PREFIXES = ['/api/', '/admin/', '/_nuxt/', '/_nitro/', '/__kestrel/']
|
|
14
|
-
const META_ROUTES = new Set(['/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt', '/redirects.json'])
|
|
15
|
-
|
|
16
|
-
// EXEMPT_PREFIXES is slash-terminated the same way (see the const's own comment), so the bare form gets
|
|
17
|
-
// the same exact-path treatment `isDeliveryExemptPath` applies to the configured entries — matching only
|
|
18
|
-
// `startsWith(prefix)` left it unmatched, contrary to `deliveryExempt`'s own documented examples
|
|
19
|
-
// (`/health`, `/feed.xml`).
|
|
20
|
-
function isExempt(path: string): boolean {
|
|
21
|
-
if (META_ROUTES.has(path)) return true
|
|
22
|
-
if (path === '/admin') return true // the bare admin root has no trailing segment to anchor a '/admin/' prefix match
|
|
23
|
-
if (EXEMPT_PREFIXES.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix))) return true
|
|
24
|
-
return isDeliveryExemptPath(path)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// A route's own trailing slash is not a distinct route: '/route/' must resolve the same snapshot as
|
|
28
|
-
// '/route'. Only the FINAL slash is stripped (one normalization pass), so a prefix match like '/admin/'
|
|
29
|
-
// against '/admin/foo/' still sees '/admin/foo' and still starts with '/admin/'. The root path is its
|
|
30
|
-
// own fixed point and is never touched.
|
|
31
|
-
function normalizeTrailingSlash(path: string): string {
|
|
32
|
-
return path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// RFC 7232 §3.2: If-None-Match may carry a comma-separated list of ETags, and a bare `*` matches any
|
|
36
|
-
// current representation (always a match, as long as the resource exists — checked by the caller).
|
|
37
|
-
// This route's ETag is always strong (`"<fingerprint>"`), so a weak validator (`W/"..."`) never matches
|
|
38
|
-
// under strong comparison, per the same section.
|
|
39
|
-
function ifNoneMatchHits(header: string | undefined, etag: string): boolean {
|
|
40
|
-
if (!header) return false
|
|
41
|
-
const trimmed = header.trim()
|
|
42
|
-
if (trimmed === '*') return true
|
|
43
|
-
return trimmed.split(',').some((candidate) => candidate.trim() === etag)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The delivery-live catch-all: under `delivery: 'live'`, answers public content traffic exclusively from
|
|
48
|
-
* `published_snapshots` — a configured redirect, 200 with the snapshot's own html, or 404. Wired as a
|
|
49
|
-
* Nitro MIDDLEWARE (see `middleware/delivery-live-catchall.ts`) rather than a route, since a middleware
|
|
50
|
-
* runs ahead of Nuxt's page router; middleware return values are ignored by h3, so this writes the
|
|
51
|
-
* response itself and returns whether it did.
|
|
52
|
-
*
|
|
53
|
-
* Returns `false` (unhandled — the caller falls through to ordinary page rendering) for everything the
|
|
54
|
-
* adapter must not touch: `delivery: 'static'` (the default), a response already sent, a non-GET/HEAD
|
|
55
|
-
* request, a non-content path (admin/api/assets/meta/consumer-exempted routes), or the producer's OWN
|
|
56
|
-
* internal render fetch. That last one is `isRendererContext()`/`import.meta.prerender` — the SAME
|
|
57
|
-
* unforgeable AsyncLocalStorage marker `pipelines/route.ts` and `00.ip-allowlist.ts` already trust for
|
|
58
|
-
* exactly this "is this our own in-process render, not an external request" question (`render-live.ts`'s
|
|
59
|
-
* `renderRouteLive` runs its `localFetch` inside `runAsRenderer`, which sets it) — NOT a request header,
|
|
60
|
-
* which any external client could forge. Without this bypass, a publish's own render would loop back
|
|
61
|
-
* into this catch-all and get served its route's CURRENT (about-to-be-superseded) snapshot instead of
|
|
62
|
-
* ever reaching a real page render, so a
|
|
63
|
-
* publish under `'live'` could never record a fresh one.
|
|
64
|
-
*/
|
|
65
|
-
export async function serveLiveRoute(event: H3Event): Promise<boolean> {
|
|
66
|
-
if (event.node?.res?.writableEnded) return false
|
|
67
|
-
if (!isDeliveryLive()) return false
|
|
68
|
-
if (import.meta.prerender === true || isRendererContext()) return false
|
|
69
|
-
const method = getMethod(event)
|
|
70
|
-
if (method !== 'GET' && method !== 'HEAD') return false
|
|
71
|
-
const path = normalizeTrailingSlash(event.path.split('?')[0] || '/')
|
|
72
|
-
if (isExempt(path)) return false
|
|
73
|
-
|
|
74
|
-
// Live mode is the app's own edge — there is no separate njs/edge script in front of it the way the
|
|
75
|
-
// static topology has, so a configured redirect has to be honoured here too, before the 404.
|
|
76
|
-
const redirect = liveRedirectFor(path)
|
|
77
|
-
if (redirect) {
|
|
78
|
-
await sendRedirect(event, redirect.target, redirect.status)
|
|
79
|
-
return true
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const snapshot = currentSnapshot(usePublishingDb().db, path)
|
|
83
|
-
if (!snapshot) {
|
|
84
|
-
setResponseStatus(event, 404)
|
|
85
|
-
await send(event, 'Not Found', 'text/plain')
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// The fingerprint is a sha256 of the exact rendered bytes (publisher.ts), so a STRONG ETag is exact:
|
|
90
|
-
// any two responses sharing it are byte-identical, never merely semantically equivalent. Cache-Control
|
|
91
|
-
// mirrors delivery-static's own policy for an html key (`cacheControlFor`/`REVALIDATE_CACHE`) so the
|
|
92
|
-
// two adapters agree on cacheability for the same snapshot. NOTE: this ETag/304 state lives only in
|
|
93
|
-
// `published_snapshots`, read fresh per request — there is no separate in-process cache to invalidate,
|
|
94
|
-
// so this is safe across multiple server processes sharing the DB.
|
|
95
|
-
const etag = `"${snapshot.fingerprint}"`
|
|
96
|
-
const cacheControl = cacheControlFor(htmlKeyForRoute(path))
|
|
97
|
-
setHeader(event, 'etag', etag)
|
|
98
|
-
if (cacheControl) setHeader(event, 'cache-control', cacheControl)
|
|
99
|
-
|
|
100
|
-
if (ifNoneMatchHits(getRequestHeader(event, 'if-none-match'), etag)) {
|
|
101
|
-
setResponseStatus(event, 304)
|
|
102
|
-
await send(event, undefined)
|
|
103
|
-
return true
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const body = Buffer.from(snapshot.html, 'utf8')
|
|
107
|
-
setHeader(event, 'content-length', body.byteLength)
|
|
108
|
-
setResponseStatus(event, 200)
|
|
109
|
-
await send(event, method === 'HEAD' ? undefined : snapshot.html, 'text/html; charset=utf-8')
|
|
110
|
-
return true
|
|
111
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { serveLiveRoute } from '../delivery-live/serve'
|
|
2
|
-
|
|
3
|
-
// Runs on every request; only intercepts when serveLiveRoute's own gate says so (delivery-live/ stays
|
|
4
|
-
// the single owner of when/how this adapter answers — see its TSDoc).
|
|
5
|
-
export default defineEventHandler(async (event) => {
|
|
6
|
-
await serveLiveRoute(event)
|
|
7
|
-
})
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { PipelineDef } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import {
|
|
3
|
-
buildPublishPipelines,
|
|
4
|
-
buildPublishRunsPipelines,
|
|
5
|
-
buildLinkPipelines,
|
|
6
|
-
buildPreviewPipelines,
|
|
7
|
-
buildRoutePipelines,
|
|
8
|
-
} from '@michaelthielemann/kestrel-publishing'
|
|
9
|
-
import { buildDeliveryLivePipelines } from '@michaelthielemann/kestrel-delivery-live'
|
|
10
|
-
|
|
11
|
-
/** The public layer's own API surface: the render entry, the preview ticket pair, the publish action and
|
|
12
|
-
* its status read, the orchestrator's admin progress read, the editor's link resolver, and the
|
|
13
|
-
* delivery-live read API. All collection-less, so each lives at `/api/<name>`. */
|
|
14
|
-
export function buildPublicPipelines(): PipelineDef[] {
|
|
15
|
-
return [
|
|
16
|
-
...buildRoutePipelines(),
|
|
17
|
-
...buildPreviewPipelines(),
|
|
18
|
-
...buildPublishPipelines(),
|
|
19
|
-
...buildPublishRunsPipelines(),
|
|
20
|
-
...buildLinkPipelines(),
|
|
21
|
-
...buildDeliveryLivePipelines(),
|
|
22
|
-
]
|
|
23
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type Database from 'better-sqlite3'
|
|
2
|
-
import { useDb } from '@michaelthielemann/kestrel-core'
|
|
3
|
-
import { ensureSnapshotTriggers } from '@michaelthielemann/kestrel-publishing'
|
|
4
|
-
|
|
5
|
-
// Layer order puts every core-layer plugin (00.migrate, 02.schema-sync) before this layer's, so
|
|
6
|
-
// `published_snapshots` already exists here for either provisioning path (a committed migration inside
|
|
7
|
-
// Kestrel's own repo, or the schema engine for a consumer layer) — except a boot before EITHER has run,
|
|
8
|
-
// which `ensureSnapshotTriggers` itself tolerates (warns, does not crash). Skipped during prerender, same
|
|
9
|
-
// as `02.schema-sync`: no DDL runs in that phase.
|
|
10
|
-
export default defineNitroPlugin(() => {
|
|
11
|
-
if (import.meta.prerender) return
|
|
12
|
-
const client = (useDb() as unknown as { $client: Database.Database }).$client
|
|
13
|
-
ensureSnapshotTriggers(client)
|
|
14
|
-
})
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { buildPublicPipelines } from '../pipelines'
|
|
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 buildPublicPipelines()) registerPipeline(def)
|
|
7
|
-
})
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { registerFieldPopulator } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { buildLinkFieldPopulators, resolveInternalHref } from '@michaelthielemann/kestrel-publishing'
|
|
3
|
-
|
|
4
|
-
// Resolve internal links to the target record's localized public path at read time (page-like targets;
|
|
5
|
-
// external/email/tel pass through). The resolver IS status-gated: a link to a DRAFT resolves to nothing and
|
|
6
|
-
// bakes `'#'`, so an unpublished slug never reaches published HTML. The href therefore encodes availability,
|
|
7
|
-
// which is why flipping a target's status re-renders its referrers; the editor warns about the resulting
|
|
8
|
-
// draft/dead links separately. (See link-resolve.ts.)
|
|
9
|
-
//
|
|
10
|
-
// Registered as `link` + `richtext` per-type populators; the shared field-tree walker dispatches them over
|
|
11
|
-
// top-level fields, block props, slots, and repeater entries.
|
|
12
|
-
export default defineNitroPlugin(() => {
|
|
13
|
-
const { link, richtext } = buildLinkFieldPopulators(resolveInternalHref)
|
|
14
|
-
registerFieldPopulator('link', link)
|
|
15
|
-
registerFieldPopulator('richtext', richtext)
|
|
16
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Effect } from 'effect'
|
|
2
|
-
import { collectionOf, registerAfterStep } from '@michaelthielemann/kestrel-core'
|
|
3
|
-
import type { StepDef } from '@michaelthielemann/kestrel-core'
|
|
4
|
-
import { outputDriver, REDIRECTS_COLLECTION, REDIRECTS_FIELD, writeRedirectsArtifact } from '@michaelthielemann/kestrel-publishing'
|
|
5
|
-
import { invalidateLiveRedirects } from '@michaelthielemann/kestrel-delivery-live'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Publish `redirects.json` on every save of the redirects singleton — deliberately decoupled from the
|
|
9
|
-
* publish cycle, so a redirect goes live without a full republish (and without an editor pressing
|
|
10
|
-
* Publish, which after ADR-0008 they otherwise would have to).
|
|
11
|
-
*
|
|
12
|
-
* A CRITICAL after-step, not a non-critical one: a save that reports success while the edge still serves
|
|
13
|
-
* the old rules is exactly the failure this feature cannot have — its failure becomes the save's response.
|
|
14
|
-
* Registered in every environment, unlike the publish plugin — that one is dev-gated because a dev publish
|
|
15
|
-
* would write Vite-dev HTML with no hashed `_nuxt`, a CONTENT problem this artifact does not have (it is
|
|
16
|
-
* `JSON.stringify` of DB rows, byte-identical either way).
|
|
17
|
-
*
|
|
18
|
-
* Where it lands is `output.dir` / the S3 prefix — the same target the publisher uses. With the classic
|
|
19
|
-
* `output.auto: false` + `driver: 'local'` build model that is NOT the deployed tree, so there a
|
|
20
|
-
* redirect goes live with the next `nuxt generate` instead; documented under Redirects in
|
|
21
|
-
* `docs/guide/redirects.md`.
|
|
22
|
-
*/
|
|
23
|
-
export default defineNitroPlugin(() => {
|
|
24
|
-
const writeRedirectsStep: StepDef = {
|
|
25
|
-
name: 'writeRedirects',
|
|
26
|
-
when: (ctx) => collectionOf(ctx).def.name === REDIRECTS_COLLECTION,
|
|
27
|
-
whenLabel: `collection is "${REDIRECTS_COLLECTION}"`,
|
|
28
|
-
fn: (ctx) => Effect.gen(function* () {
|
|
29
|
-
const row = ctx.output as Record<string, unknown>
|
|
30
|
-
// The row is already committed regardless of what the static artifact write below does (see its
|
|
31
|
-
// own comment) — the live catch-all reads straight from the row, so its cache drops unconditionally,
|
|
32
|
-
// in lockstep with every save.
|
|
33
|
-
invalidateLiveRedirects()
|
|
34
|
-
// A JS try/catch does NOT observe an Effect failure crossing a `yield*` — only Effect's own
|
|
35
|
-
// combinators do — so the survivor rewrap has to be Effect.catchAll, not a wrapping try/catch.
|
|
36
|
-
yield* Effect.tryPromise({ try: () => writeRedirectsArtifact(row[REDIRECTS_FIELD], outputDriver()), catch: (error) => error }).pipe(
|
|
37
|
-
Effect.catchAll((error) => Effect.sync(() => {
|
|
38
|
-
// The row is already committed (CRUD holds no transaction), so the only honest message is
|
|
39
|
-
// "saved, but not live". Short and ASCII on purpose: h3 truncates nothing but the reason phrase is
|
|
40
|
-
// the wire's, and the cause goes in `data` where nothing can strip it. `savedUpdatedAt` lets the
|
|
41
|
-
// editor rebaseline on this failure the same way it would on success — it only rebaselines then.
|
|
42
|
-
throw createError({
|
|
43
|
-
statusCode: 500,
|
|
44
|
-
statusMessage: 'Redirects saved, but publishing redirects.json failed. Save again to retry.',
|
|
45
|
-
data: {
|
|
46
|
-
cause: (error as Error)?.message ?? String(error),
|
|
47
|
-
savedUpdatedAt: new Date(row.updatedAt as string | number).getTime(),
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
})),
|
|
51
|
-
)
|
|
52
|
-
}),
|
|
53
|
-
}
|
|
54
|
-
registerAfterStep({ step: writeRedirectsStep, critical: true, ops: ['updateOne'] })
|
|
55
|
-
})
|