@michaelthielemann/kestrel 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -110
- package/kestrel.config.ts +1 -1
- package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
- package/layers/access/server/middleware/access-guard.ts +25 -23
- package/layers/access/server/types.d.ts +1 -2
- package/layers/admin/app/components/AdminAccount.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +1 -2
- package/layers/admin/app/components/BlockPreview.vue +17 -5
- package/layers/admin/app/components/BlocksBody.vue +1 -1
- package/layers/admin/app/components/CollectionEditor.vue +35 -16
- package/layers/admin/app/components/CollectionList.vue +34 -3
- package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
- package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
- package/layers/admin/app/components/CollectionListPager.vue +1 -1
- package/layers/admin/app/components/CollectionListTable.vue +41 -2
- package/layers/admin/app/components/EditorStatus.vue +8 -7
- package/layers/admin/app/components/LocaleBar.vue +3 -1
- package/layers/admin/app/components/PageFields.vue +2 -4
- package/layers/admin/app/components/SeoFields.vue +2 -4
- package/layers/admin/app/components/SingletonEditor.vue +3 -2
- package/layers/admin/app/composables/useAuth.ts +3 -3
- package/layers/admin/app/composables/useBlockTree.ts +1 -1
- package/layers/admin/app/composables/useBlocks.ts +1 -1
- package/layers/admin/app/composables/useCollectionOps.ts +45 -8
- package/layers/admin/app/composables/useCollections.ts +1 -2
- package/layers/admin/app/composables/useEditForm.ts +35 -17
- package/layers/admin/app/composables/useLinkResolver.ts +1 -1
- package/layers/admin/app/composables/useListBatchActions.ts +20 -3
- package/layers/admin/app/composables/useListColumns.ts +1 -1
- package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
- package/layers/admin/app/composables/useListRows.ts +7 -2
- package/layers/admin/app/composables/useListUrlState.ts +1 -1
- package/layers/admin/app/composables/usePublishStatus.ts +2 -2
- package/layers/admin/app/layouts/admin.vue +0 -1
- package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
- package/layers/admin/app/pages/admin/index.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +2 -2
- package/layers/admin/app/utils/block-tree.ts +1 -1
- package/layers/admin/app/utils/collection-ops.ts +28 -3
- package/layers/admin/app/utils/edit-form.ts +34 -2
- package/layers/admin/app/utils/editor-expose.ts +2 -0
- package/layers/admin/app/utils/editor-form-context.ts +1 -2
- package/layers/admin/app/utils/field-keys.ts +1 -1
- package/layers/admin/app/utils/list-columns.ts +3 -2
- package/layers/admin/app/utils/list-query.ts +2 -2
- package/layers/admin/app/utils/populate-blocks.ts +2 -2
- package/layers/admin/app/utils/reauth.ts +5 -6
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
- package/layers/collections/server/plugins/01.register.ts +9 -12
- package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
- package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
- package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
- package/layers/core/modules/auto-discovery/index.ts +68 -11
- package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
- package/layers/core/modules/auto-discovery/scan.ts +77 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
- package/layers/core/modules/component-namespace/index.ts +24 -0
- package/layers/core/modules/kestrel/index.ts +6 -1
- package/layers/core/modules/plugin-order/index.ts +69 -0
- package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/api/[...path].ts +98 -0
- package/layers/core/server/plugins/00.config.ts +12 -0
- package/layers/core/server/plugins/00.migrate.ts +1 -0
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
- package/layers/core/server/plugins/02.schema-sync.ts +2 -7
- package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
- package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
- package/layers/core/server/tasks/db/migrate-module.ts +83 -0
- package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
- package/layers/core/server/tasks/db/migrate.ts +2 -6
- package/layers/core/server/utils/server-config.ts +66 -3
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
- package/layers/media/app/components/KestrelImg.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +2 -2
- package/layers/media/app/components/field/Media.vue +1 -1
- package/layers/media/app/composables/useMediaResolver.ts +1 -1
- package/layers/media/app/composables/useMediaUpload.ts +1 -1
- package/layers/media/app/composables/useMediaVariant.ts +2 -2
- package/layers/media/app/utils/ai-disclosure.ts +1 -1
- package/layers/media/app/utils/library.ts +1 -1
- package/layers/media/app/utils/ops.ts +1 -3
- package/layers/media/app/utils/picture-model.ts +2 -2
- package/layers/media/app/utils/variant-name.ts +1 -1
- package/layers/media/server/middleware/ondemand-variants.ts +6 -5
- package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
- package/layers/media/server/plugins/02.register-media.ts +10 -7
- package/layers/media/server/plugins/04.variant-capture.ts +12 -8
- package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
- package/layers/media/server/tasks/media/backfill.ts +3 -4
- package/layers/public/app/components/KestrelLink.vue +1 -1
- package/layers/public/app/pages/[...slug].vue +1 -1
- package/layers/public/app/pages/__kestrel/preview.vue +2 -2
- package/layers/public/app/utils/json-ld.ts +1 -1
- package/layers/public/app/utils/link-href.ts +1 -1
- package/layers/public/app/utils/page-head.ts +1 -1
- package/layers/public/modules/deploy-output/index.ts +4 -6
- package/layers/public/modules/prerender-routes/index.ts +4 -4
- package/layers/public/modules/prerender-routes/routes.ts +1 -1
- package/layers/public/modules/prune-media/index.ts +2 -2
- package/layers/public/server/delivery-live/serve.ts +111 -0
- package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
- package/layers/public/server/pipelines/index.ts +23 -0
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
- package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
- package/layers/public/server/plugins/02.register-links.ts +2 -3
- package/layers/public/server/plugins/03.redirects.ts +41 -23
- package/layers/public/server/plugins/05.plan-publish.ts +5 -0
- package/layers/public/server/plugins/zz.publish.ts +79 -24
- package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
- package/layers/public/server/routes/llms.txt.get.ts +4 -2
- package/layers/public/server/routes/redirects.json.get.ts +2 -2
- package/layers/public/server/routes/robots.txt.get.ts +2 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
- package/layers/public/server/tasks/publish/run.ts +9 -4
- package/layers/public/server/utils/publish/render-live.ts +26 -0
- package/layers/ui/app/components/field/Choice.vue +1 -1
- package/layers/ui/app/components/field/Datetime.vue +1 -1
- package/layers/ui/app/components/field/Json.vue +1 -2
- package/layers/ui/app/components/field/Layout.vue +2 -4
- package/layers/ui/app/components/field/Link.vue +1 -1
- package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
- package/layers/ui/app/components/field/Number.vue +2 -2
- package/layers/ui/app/components/field/Relation.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +1 -3
- package/layers/ui/app/components/field/Slug.vue +2 -3
- package/layers/ui/app/components/field/Text.vue +1 -1
- package/layers/ui/app/components/ui/Richtext.vue +1 -1
- package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
- package/layers/ui/app/composables/useLinkField.ts +1 -1
- package/layers/ui/app/composables/useRepeater.ts +1 -1
- package/layers/ui/app/i18n/de.ts +7 -7
- package/layers/ui/app/i18n/en.ts +7 -39
- package/layers/ui/app/utils/field-component.ts +1 -1
- package/layers/ui/app/utils/field-registry.ts +1 -1
- package/layers/ui/app/utils/icons.ts +2 -6
- package/layers/ui/app/utils/localized.ts +1 -1
- package/nuxt.config.ts +1 -1
- package/package.json +53 -19
- package/scripts/consumer-template-ci.mjs +157 -0
- package/scripts/coverage-all.mjs +163 -0
- package/scripts/dashboard.mjs +89 -0
- package/scripts/hash-password.mjs +1 -1
- package/scripts/lib/coverage-model.mjs +51 -0
- package/scripts/port-graph.mjs +350 -0
- package/scripts/typecheck.mjs +11 -0
- package/templates/starter/app/blocks/Prose.vue +1 -1
- package/layers/access/server/utils/csrf.ts +0 -39
- package/layers/access/server/utils/grant-registry.ts +0 -36
- package/layers/access/server/utils/guard.ts +0 -44
- package/layers/access/server/utils/ip-allowlist.ts +0 -79
- package/layers/access/server/utils/policy.ts +0 -98
- package/layers/access/server/utils/public-resources.ts +0 -11
- package/layers/access/server/utils/render-context.ts +0 -41
- package/layers/access/server/utils/require-admin.ts +0 -15
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
- package/layers/auth/server/api/auth/login.post.ts +0 -38
- package/layers/auth/server/api/auth/logout.post.ts +0 -9
- package/layers/auth/server/api/auth/session.get.ts +0 -1
- package/layers/auth/server/utils/admin-credential.ts +0 -42
- package/layers/auth/server/utils/client-ip.ts +0 -88
- package/layers/auth/server/utils/login-throttle.ts +0 -126
- package/layers/auth/server/utils/password.ts +0 -52
- package/layers/auth/server/utils/session-cookie.ts +0 -44
- package/layers/auth/server/utils/session-epoch.ts +0 -96
- package/layers/auth/server/utils/session.ts +0 -88
- package/layers/collections/server/collections/pages.ts +0 -19
- package/layers/collections/server/utils/populate-relations.ts +0 -79
- package/layers/core/app/utils/bulk-actions.ts +0 -9
- package/layers/core/app/utils/filter-ops.ts +0 -98
- package/layers/core/app/utils/list-limits.ts +0 -27
- package/layers/core/app/utils/locale-path.ts +0 -12
- package/layers/core/app/utils/slugify.ts +0 -16
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
- package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
- package/layers/core/server/api/[collection]/[id].get.ts +0 -11
- package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
- package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
- package/layers/core/server/api/[collection]/index.get.ts +0 -24
- package/layers/core/server/api/[collection]/index.post.ts +0 -8
- package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
- package/layers/core/server/api/[collection]/index.put.ts +0 -24
- package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
- package/layers/core/server/api/[collection]/options.get.ts +0 -18
- package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
- package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
- package/layers/core/server/api/[collection]/translations.get.ts +0 -25
- package/layers/core/server/api/collections/[name].get.ts +0 -4
- package/layers/core/server/api/collections/index.get.ts +0 -3
- package/layers/core/server/api/references/broken.get.ts +0 -9
- package/layers/core/server/api/references/referrers.get.ts +0 -24
- package/layers/core/server/database/record-refs.ts +0 -20
- package/layers/core/server/plugins/03.record-refs.ts +0 -12
- package/layers/core/server/schema/bootstrap.ts +0 -41
- package/layers/core/server/schema/desired.ts +0 -99
- package/layers/core/server/schema/dev-sync.ts +0 -23
- package/layers/core/server/schema/dialect.ts +0 -61
- package/layers/core/server/schema/diff.ts +0 -110
- package/layers/core/server/schema/introspect.ts +0 -68
- package/layers/core/server/schema/model.ts +0 -61
- package/layers/core/server/schema/render-sqlite.ts +0 -89
- package/layers/core/server/schema/sync.ts +0 -201
- package/layers/core/server/utils/blocks.ts +0 -6
- package/layers/core/server/utils/collection-types.ts +0 -28
- package/layers/core/server/utils/crud.ts +0 -578
- package/layers/core/server/utils/db.ts +0 -26
- package/layers/core/server/utils/defineCollection.ts +0 -213
- package/layers/core/server/utils/duplicate.ts +0 -162
- package/layers/core/server/utils/field-layout.ts +0 -125
- package/layers/core/server/utils/filter-predicate.ts +0 -101
- package/layers/core/server/utils/http.ts +0 -50
- package/layers/core/server/utils/if-unmodified.ts +0 -13
- package/layers/core/server/utils/kestrel-config.ts +0 -451
- package/layers/core/server/utils/key-lock.ts +0 -33
- package/layers/core/server/utils/locale.ts +0 -53
- package/layers/core/server/utils/page-route.ts +0 -58
- package/layers/core/server/utils/page-slug.ts +0 -127
- package/layers/core/server/utils/picker.ts +0 -82
- package/layers/core/server/utils/populate.ts +0 -50
- package/layers/core/server/utils/prerender-memo.ts +0 -23
- package/layers/core/server/utils/read-capture.ts +0 -37
- package/layers/core/server/utils/record-ref-index.ts +0 -200
- package/layers/core/server/utils/registry.ts +0 -24
- package/layers/core/server/utils/resolve-scope.ts +0 -79
- package/layers/core/server/utils/seo.ts +0 -42
- package/layers/core/server/utils/serialize-collection.ts +0 -147
- package/layers/core/server/utils/sql.ts +0 -2
- package/layers/core/server/utils/storage.local.ts +0 -117
- package/layers/core/server/utils/storage.s3.ts +0 -197
- package/layers/core/server/utils/storage.ts +0 -90
- package/layers/core/server/utils/write-effects.ts +0 -40
- package/layers/core/server/utils/write-events.ts +0 -31
- package/layers/fields/app/utils/condition.ts +0 -71
- package/layers/fields/app/utils/define-block.ts +0 -13
- package/layers/fields/app/utils/field-constraints.ts +0 -17
- package/layers/fields/app/utils/field-meta.ts +0 -33
- package/layers/fields/app/utils/field-validate.ts +0 -75
- package/layers/fields/app/utils/field-value.ts +0 -7
- package/layers/fields/app/utils/richtext-links.ts +0 -48
- package/layers/fields/server/api/blocks/index.get.ts +0 -9
- package/layers/fields/server/field-registry/index.ts +0 -249
- package/layers/fields/server/field-registry/naming.ts +0 -10
- package/layers/fields/server/field-registry/sanitize.ts +0 -43
- package/layers/fields/server/field-registry/types.ts +0 -19
- package/layers/fields/server/utils/block-ids.ts +0 -43
- package/layers/fields/server/utils/block-populate.ts +0 -41
- package/layers/fields/server/utils/buildCollection.ts +0 -90
- package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
- package/layers/fields/server/utils/buildTable.ts +0 -113
- package/layers/fields/server/utils/conditional-required.ts +0 -25
- package/layers/fields/server/utils/defineBlock.ts +0 -86
- package/layers/fields/server/utils/defineFieldType.ts +0 -33
- package/layers/fields/server/utils/extract-refs.ts +0 -152
- package/layers/fields/server/utils/field-populate.ts +0 -68
- package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
- package/layers/media/server/api/media/[id].delete.ts +0 -18
- package/layers/media/server/api/media/[id].get.ts +0 -10
- package/layers/media/server/api/media/[id].patch.test.ts +0 -66
- package/layers/media/server/api/media/[id].patch.ts +0 -65
- package/layers/media/server/api/media/backfill.post.ts +0 -18
- package/layers/media/server/api/media/copy.post.ts +0 -7
- package/layers/media/server/api/media/delete.post.ts +0 -13
- package/layers/media/server/api/media/folders.post.ts +0 -18
- package/layers/media/server/api/media/index.get.ts +0 -15
- package/layers/media/server/api/media/index.post.ts +0 -173
- package/layers/media/server/api/media/library.get.ts +0 -17
- package/layers/media/server/api/media/move.post.ts +0 -7
- package/layers/media/server/api/media/rename.post.ts +0 -7
- package/layers/media/server/api/media/resolve.get.ts +0 -13
- package/layers/media/server/collections/media-settings.ts +0 -25
- package/layers/media/server/collections/media.ts +0 -43
- package/layers/media/server/database/folders.ts +0 -10
- package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
- package/layers/media/server/schema-tables/folders.ts +0 -4
- package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
- package/layers/media/server/utils/ai-signal-detect.ts +0 -155
- package/layers/media/server/utils/backfill.ts +0 -144
- package/layers/media/server/utils/derive.ts +0 -59
- package/layers/media/server/utils/folder-paths.ts +0 -42
- package/layers/media/server/utils/folders.ts +0 -12
- package/layers/media/server/utils/library.ts +0 -91
- package/layers/media/server/utils/media-cleanup.ts +0 -27
- package/layers/media/server/utils/media-enabled.ts +0 -23
- package/layers/media/server/utils/media-ops.ts +0 -118
- package/layers/media/server/utils/media-write.ts +0 -13
- package/layers/media/server/utils/naming.ts +0 -52
- package/layers/media/server/utils/ondemand.ts +0 -69
- package/layers/media/server/utils/persist-upload.ts +0 -62
- package/layers/media/server/utils/populate.ts +0 -47
- package/layers/media/server/utils/record.ts +0 -71
- package/layers/media/server/utils/relocate-ops.ts +0 -341
- package/layers/media/server/utils/relocate-plan.ts +0 -63
- package/layers/media/server/utils/resolve.ts +0 -98
- package/layers/media/server/utils/sanitize-svg.ts +0 -12
- package/layers/media/server/utils/sniff.ts +0 -68
- package/layers/media/server/utils/storage-relocate.ts +0 -139
- package/layers/media/server/utils/translations.ts +0 -19
- package/layers/media/server/utils/usages.ts +0 -66
- package/layers/media/server/utils/variant-capture.ts +0 -70
- package/layers/media/server/utils/variants.ts +0 -123
- package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
- package/layers/public/server/api/links/resolve.get.ts +0 -19
- package/layers/public/server/api/preview.get.ts +0 -28
- package/layers/public/server/api/preview.post.ts +0 -93
- package/layers/public/server/api/publish-status.get.ts +0 -58
- package/layers/public/server/api/publish.post.ts +0 -84
- package/layers/public/server/api/route.get.ts +0 -54
- package/layers/public/server/collections/redirects.ts +0 -75
- package/layers/public/server/collections/site.ts +0 -44
- package/layers/public/server/database/publish-deps.ts +0 -12
- package/layers/public/server/database/publish-status.ts +0 -19
- package/layers/public/server/schema-tables/publish-deps.ts +0 -4
- package/layers/public/server/schema-tables/publish-status.ts +0 -4
- package/layers/public/server/utils/link-resolve.ts +0 -38
- package/layers/public/server/utils/llms-full.ts +0 -125
- package/layers/public/server/utils/llms.ts +0 -52
- package/layers/public/server/utils/page-resolve.ts +0 -208
- package/layers/public/server/utils/populate-links.ts +0 -66
- package/layers/public/server/utils/preview-token.ts +0 -109
- package/layers/public/server/utils/publish/coalesce.ts +0 -22
- package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
- package/layers/public/server/utils/publish/deps.ts +0 -87
- package/layers/public/server/utils/publish/invalidation.ts +0 -225
- package/layers/public/server/utils/publish/pending.ts +0 -74
- package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
- package/layers/public/server/utils/publish/publish-status.ts +0 -72
- package/layers/public/server/utils/publish/publisher.ts +0 -362
- package/layers/public/server/utils/publish/queue.ts +0 -67
- package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
- package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
- package/layers/public/server/utils/publish/route-for-record.ts +0 -16
- package/layers/public/server/utils/publish/route-keys.ts +0 -13
- package/layers/public/server/utils/richtext-markdown.ts +0 -260
- package/layers/public/server/utils/site-url.ts +0 -40
- package/layers/public/server/utils/sitemap.ts +0 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michaelthielemann/kestrel",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"kestrel": "./scripts/kestrel.mjs"
|
|
26
26
|
},
|
|
27
|
-
"packageManager": "pnpm@11.9.0",
|
|
28
27
|
"publishConfig": {
|
|
29
28
|
"access": "public",
|
|
30
29
|
"provenance": true
|
|
@@ -45,21 +44,6 @@
|
|
|
45
44
|
"node": ">=22.12.0"
|
|
46
45
|
},
|
|
47
46
|
"//lint": "The root eslint.config.mjs imports the generated playground/.nuxt/eslint.config.mjs (Nuxt-aware: auto-import globals for every layer + extension) — `nuxt prepare` must run first or that import 404s on a fresh checkout. Cheap on a warm .nuxt cache, so it's chained rather than a separate step.",
|
|
48
|
-
"scripts": {
|
|
49
|
-
"dev": "nuxt dev",
|
|
50
|
-
"build": "nuxt build",
|
|
51
|
-
"preview": "nuxt preview",
|
|
52
|
-
"generate": "nuxt generate",
|
|
53
|
-
"db:generate": "drizzle-kit generate",
|
|
54
|
-
"db:migrate": "drizzle-kit migrate",
|
|
55
|
-
"test": "vitest run",
|
|
56
|
-
"test:watch": "vitest",
|
|
57
|
-
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
58
|
-
"test:nuxt": "vitest run --config vitest.nuxt.config.ts",
|
|
59
|
-
"typecheck": "node scripts/typecheck.mjs",
|
|
60
|
-
"lint": "nuxt prepare playground && eslint .",
|
|
61
|
-
"hash-password": "node scripts/hash-password.mjs"
|
|
62
|
-
},
|
|
63
47
|
"dependencies": {
|
|
64
48
|
"@babel/parser": "^7.29.8",
|
|
65
49
|
"@internationalized/date": "^3.12.3",
|
|
@@ -78,10 +62,12 @@
|
|
|
78
62
|
"dompurify": "^3.4.13",
|
|
79
63
|
"drizzle-orm": "^0.45.2",
|
|
80
64
|
"drizzle-zod": "^0.8.3",
|
|
65
|
+
"effect": "~3.22.1",
|
|
81
66
|
"exifr": "^7.1.3",
|
|
82
67
|
"file-type": "^22.0.1",
|
|
83
68
|
"jsdom": "^29.1.1",
|
|
84
69
|
"nanoid": "^5.1.16",
|
|
70
|
+
"nitropack": "^2.13.4",
|
|
85
71
|
"nuxt": "^4.5.2",
|
|
86
72
|
"reka-ui": "^2.10.1",
|
|
87
73
|
"sanitize-html": "^2.17.6",
|
|
@@ -89,7 +75,17 @@
|
|
|
89
75
|
"sharp": "^0.35.3",
|
|
90
76
|
"thumbhash": "^0.1.1",
|
|
91
77
|
"typescript": "^6.0.3",
|
|
92
|
-
"zod": "^4.4.3"
|
|
78
|
+
"zod": "^4.4.3",
|
|
79
|
+
"@michaelthielemann/kestrel-access": "0.1.0",
|
|
80
|
+
"@michaelthielemann/kestrel-auth": "0.1.0",
|
|
81
|
+
"@michaelthielemann/kestrel-core": "0.1.0",
|
|
82
|
+
"@michaelthielemann/kestrel-contracts": "0.1.0",
|
|
83
|
+
"@michaelthielemann/kestrel-collections": "0.1.0",
|
|
84
|
+
"@michaelthielemann/kestrel-delivery-live": "0.1.0",
|
|
85
|
+
"@michaelthielemann/kestrel-delivery-static": "0.1.0",
|
|
86
|
+
"@michaelthielemann/kestrel-fields": "0.1.0",
|
|
87
|
+
"@michaelthielemann/kestrel-media": "0.1.0",
|
|
88
|
+
"@michaelthielemann/kestrel-publishing": "0.1.0"
|
|
93
89
|
},
|
|
94
90
|
"//peers": "Framework singletons the layer sources import by bare specifier. Peers, not dependencies: a second copy breaks identity (two Vue instances; an h3 v1 `createError` handed to an h3 v2 error handler, which surfaces our 400/404/409 as bare 500s). All optional, because kestrel's own `nuxt` dependency already supplies them transitively — the entry pins identity where a copy exists rather than demanding one, and a required peer would ERESOLVE-fail npm installs on any Nuxt whose bundled range we did not anticipate. Each is also a devDependency so this repo resolves it without auto-install-peers writing a phantom root dependency into the lockfile.",
|
|
95
91
|
"peerDependencies": {
|
|
@@ -113,20 +109,58 @@
|
|
|
113
109
|
}
|
|
114
110
|
},
|
|
115
111
|
"devDependencies": {
|
|
112
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
113
|
+
"@fast-check/vitest": "^0.4.1",
|
|
114
|
+
"@microsoft/api-extractor": "^7.58.13",
|
|
116
115
|
"@nuxt/eslint": "^1.9.0",
|
|
117
116
|
"@nuxt/kit": "^4.5.2",
|
|
118
117
|
"@nuxt/test-utils": "^4.1.0",
|
|
118
|
+
"@stryker-mutator/core": "^10.0.0",
|
|
119
|
+
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
119
120
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
121
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
120
122
|
"@vue/test-utils": "^2.4.11",
|
|
121
123
|
"drizzle-kit": "^0.31.10",
|
|
122
124
|
"eslint": "^9.39.1",
|
|
125
|
+
"eslint-plugin-boundaries": "^7.2.0",
|
|
126
|
+
"eslint-plugin-jsdoc": "^64.2.1",
|
|
127
|
+
"eslint-plugin-tsdoc": "^0.5.2",
|
|
123
128
|
"eslint-plugin-vuejs-accessibility": "^2.4.1",
|
|
129
|
+
"fast-check": "^4.9.0",
|
|
124
130
|
"h3": "^1.15.11",
|
|
125
131
|
"happy-dom": "^20.11.2",
|
|
126
132
|
"playwright-core": "^1.62.1",
|
|
133
|
+
"publint": "^0.3.24",
|
|
134
|
+
"typedoc": "^0.28.20",
|
|
127
135
|
"vitest": "^4.1.10",
|
|
136
|
+
"vitest-openapi": "^1.0.3",
|
|
128
137
|
"vue": "3.5.35",
|
|
129
138
|
"vue-router": "^5.2.0",
|
|
130
139
|
"vue-tsc": "^3.3.9"
|
|
140
|
+
},
|
|
141
|
+
"scripts": {
|
|
142
|
+
"dev": "nuxt dev",
|
|
143
|
+
"build": "nuxt build",
|
|
144
|
+
"build:packages": "pnpm -F @michaelthielemann/kestrel-contracts build && pnpm -F @michaelthielemann/kestrel-core build && pnpm -F @michaelthielemann/kestrel-fields build && pnpm -F @michaelthielemann/kestrel-auth build && pnpm -F @michaelthielemann/kestrel-access build && pnpm -F @michaelthielemann/kestrel-media build && pnpm -F @michaelthielemann/kestrel-collections build && pnpm -F @michaelthielemann/kestrel-publishing build && pnpm -F @michaelthielemann/kestrel-delivery-live build && pnpm -F @michaelthielemann/kestrel-delivery-static build",
|
|
145
|
+
"preview": "nuxt preview",
|
|
146
|
+
"generate": "nuxt generate",
|
|
147
|
+
"db:generate": "drizzle-kit generate",
|
|
148
|
+
"db:migrate": "drizzle-kit migrate",
|
|
149
|
+
"test": "vitest run",
|
|
150
|
+
"test:coverage": "node scripts/coverage-all.mjs",
|
|
151
|
+
"test:watch": "vitest",
|
|
152
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
153
|
+
"test:nuxt": "vitest run --config vitest.nuxt.config.ts",
|
|
154
|
+
"test:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r test",
|
|
155
|
+
"typecheck": "node scripts/typecheck.mjs",
|
|
156
|
+
"typecheck:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r typecheck",
|
|
157
|
+
"lint": "nuxt prepare playground && eslint .",
|
|
158
|
+
"docs:api": "nuxt prepare playground && typedoc",
|
|
159
|
+
"api:check": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run -c packages/kestrel-core/api-extractor.json && api-extractor run -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run -c packages/kestrel-fields/api-extractor.json && api-extractor run -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run -c packages/kestrel-delivery-static/api-extractor.json",
|
|
160
|
+
"api:update": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run --local -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run --local -c packages/kestrel-core/api-extractor.json && api-extractor run --local -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run --local -c packages/kestrel-fields/api-extractor.json && api-extractor run --local -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run --local -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run --local -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run --local -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run --local -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run --local -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run --local -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run --local -c packages/kestrel-delivery-static/api-extractor.json",
|
|
161
|
+
"pkg:lint": "publint . && pnpm -F @michaelthielemann/kestrel-contracts build && publint packages/kestrel-contracts && attw --pack packages/kestrel-contracts --profile esm-only && pnpm -F @michaelthielemann/kestrel-core build && publint packages/kestrel-core && attw --pack packages/kestrel-core --profile esm-only && pnpm -F @michaelthielemann/kestrel-fields build && publint packages/kestrel-fields && attw --pack packages/kestrel-fields --profile esm-only && pnpm -F @michaelthielemann/kestrel-auth build && publint packages/kestrel-auth && attw --pack packages/kestrel-auth --profile esm-only && pnpm -F @michaelthielemann/kestrel-access build && publint packages/kestrel-access && attw --pack packages/kestrel-access --profile esm-only && pnpm -F @michaelthielemann/kestrel-media build && publint packages/kestrel-media && attw --pack packages/kestrel-media --profile esm-only && pnpm -F @michaelthielemann/kestrel-collections build && publint packages/kestrel-collections && attw --pack packages/kestrel-collections --profile esm-only && pnpm -F @michaelthielemann/kestrel-publishing build && publint packages/kestrel-publishing && attw --pack packages/kestrel-publishing --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-live build && publint packages/kestrel-delivery-live && attw --pack packages/kestrel-delivery-live --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-static build && publint packages/kestrel-delivery-static && attw --pack packages/kestrel-delivery-static --profile esm-only",
|
|
162
|
+
"ci:consumer-template": "node scripts/consumer-template-ci.mjs",
|
|
163
|
+
"dashboard": "npx tsx scripts/dashboard.mjs",
|
|
164
|
+
"hash-password": "node scripts/hash-password.mjs"
|
|
131
165
|
}
|
|
132
|
-
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The ONLY real package-consumption test. Every earlier gate (api:check, pkg:lint) inspects one
|
|
2
|
+
// package's own tarball in isolation; this script is the one that scaffolds an actual consumer from
|
|
3
|
+
// templates/starter (via create-kestrel's real CLI), packs the engine + every @michaelthielemann/kestrel-* package the way
|
|
4
|
+
// `npm publish` would, installs them into that consumer with a real `npm install` (no workspace protocol,
|
|
5
|
+
// no symlinks back into this repo), builds it, boots the production server, and hits it over real HTTP.
|
|
6
|
+
//
|
|
7
|
+
// Local: `pnpm ci:consumer-template`. CI: the `consumer-template` job in .github/workflows/ci.yml.
|
|
8
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
9
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
13
|
+
|
|
14
|
+
const root = process.cwd()
|
|
15
|
+
const log = (msg) => console.log(`[consumer-template-ci] ${msg}`)
|
|
16
|
+
const run = (cmd, args, opts = {}) => execFileSync(cmd, args, { stdio: 'inherit', ...opts })
|
|
17
|
+
|
|
18
|
+
// @michaelthielemann/kestrel-* in the order `pnpm --filter "@michaelthielemann/kestrel-*" -r exec` resolves them (a real topological walk of
|
|
19
|
+
// the workspace graph, not a hand guess) — mirrors the publish order in .github/workflows/release.yml.
|
|
20
|
+
const PACKAGES = ['contracts', 'core', 'auth', 'access', 'fields', 'collections', 'media', 'publishing', 'delivery-live', 'delivery-static']
|
|
21
|
+
|
|
22
|
+
const work = mkdtempSync(join(tmpdir(), 'kestrel-consumer-ci-'))
|
|
23
|
+
const tarballDir = join(work, 'tarballs')
|
|
24
|
+
const consumerDir = join(work, 'site')
|
|
25
|
+
mkdirSync(tarballDir, { recursive: true })
|
|
26
|
+
log(`work dir: ${work}`)
|
|
27
|
+
|
|
28
|
+
let server
|
|
29
|
+
try {
|
|
30
|
+
log('building @michaelthielemann/kestrel-* packages (dist/ must exist before pnpm pack)')
|
|
31
|
+
run('pnpm', ['--filter', '@michaelthielemann/kestrel-*', '-r', 'build'], { cwd: root })
|
|
32
|
+
|
|
33
|
+
log('packing the engine + every @michaelthielemann/kestrel-* package as npm publish would')
|
|
34
|
+
const tarball = {}
|
|
35
|
+
for (const dir of ['.', ...PACKAGES.map((n) => `packages/kestrel-${n}`)]) {
|
|
36
|
+
const out = execFileSync('pnpm', ['pack', '--pack-destination', tarballDir], { cwd: join(root, dir) }).toString().trim()
|
|
37
|
+
const file = out.split('\n').pop().trim()
|
|
38
|
+
const name = JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8')).name
|
|
39
|
+
tarball[name] = file
|
|
40
|
+
log(` ${name} -> ${file}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log('scaffolding a consumer from templates/starter via the real create-kestrel CLI')
|
|
44
|
+
const password = 'consumer-ci-password'
|
|
45
|
+
run('node', [join(root, 'packages/create-kestrel/index.mjs'), consumerDir, '--yes', '--password', password], { cwd: work })
|
|
46
|
+
|
|
47
|
+
log('pointing the scaffolded package.json at the packed tarballs (no registry, no workspace protocol)')
|
|
48
|
+
const pkgPath = join(consumerDir, 'package.json')
|
|
49
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
50
|
+
pkg.dependencies['@michaelthielemann/kestrel'] = `file:${tarball['@michaelthielemann/kestrel']}`
|
|
51
|
+
// npm's `overrides` redirects a dependency AT ANY DEPTH — the engine tarball's own manifest declares
|
|
52
|
+
// "@michaelthielemann/kestrel-core": "0.1.0" etc. (pnpm pack already rewrote the workspace:* protocol to that real
|
|
53
|
+
// version), which would otherwise 404 against the real registry since these packages are not published
|
|
54
|
+
// yet in this checkout's tag. This is what actually forces npm's resolver to read each package's real
|
|
55
|
+
// `exports` map / `main` / `types` from a standalone tarball, the same way a real end user's install
|
|
56
|
+
// would — not a symlink into this repo's own packages/*/src.
|
|
57
|
+
pkg.overrides = Object.fromEntries(PACKAGES.map((n) => [`@michaelthielemann/kestrel-${n}`, `file:${tarball[`@michaelthielemann/kestrel-${n}`]}`]))
|
|
58
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
59
|
+
|
|
60
|
+
// The runner's ambient npm (10.9.8 as of this writing) hits a real npm/arborist bug resolving nuxt's
|
|
61
|
+
// large optional-peer set (`TypeError: Cannot read properties of null (reading 'edgesOut')`, inside
|
|
62
|
+
// `#loadPeerSet`) — reproduced directly, unrelated to anything this script does. npm >= 11 does not
|
|
63
|
+
// have it. `npx --yes npm@11` sidesteps needing a global npm upgrade (which needs root the runner may
|
|
64
|
+
// not have) while still using real npm, not pnpm.
|
|
65
|
+
log('npm install (real npm 11 via npx — the most representative simulation of an actual end user)')
|
|
66
|
+
run('npx', ['--yes', 'npm@11', 'install'], { cwd: consumerDir })
|
|
67
|
+
|
|
68
|
+
log('verifying no @michaelthielemann/kestrel-* dependency resolved back into this repo (would prove nothing)')
|
|
69
|
+
for (const n of PACKAGES) {
|
|
70
|
+
let resolved = ''
|
|
71
|
+
try {
|
|
72
|
+
resolved = execFileSync('npm', ['ls', `@michaelthielemann/kestrel-${n}`, '--json'], { cwd: consumerDir }).toString()
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// `npm ls` exits non-zero on a peer-dep mismatch warning even when the tree itself resolved fine;
|
|
75
|
+
// its stdout (on the error object) still carries the real tree, which is what matters here.
|
|
76
|
+
resolved = err.stdout?.toString() ?? ''
|
|
77
|
+
}
|
|
78
|
+
if (resolved.includes(root)) throw new Error(`@michaelthielemann/kestrel-${n} resolved into ${root} — the override did not take effect`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
log('reading the .env create-kestrel wrote (session secret + admin password hash)')
|
|
82
|
+
const env = Object.fromEntries(
|
|
83
|
+
readFileSync(join(consumerDir, '.env'), 'utf8')
|
|
84
|
+
.split('\n')
|
|
85
|
+
.map((l) => /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(l))
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.map((m) => [m[1], m[2]]),
|
|
88
|
+
)
|
|
89
|
+
// KESTREL_SECURE_COOKIES=false is what create-kestrel writes for a brand-new site (local http dev) —
|
|
90
|
+
// the app itself refuses it under a production boot ("KESTREL_SECURE_COOKIES=false is not allowed in
|
|
91
|
+
// production"), which is the correct, secure-by-default behavior, not a bug this script should route
|
|
92
|
+
// around by keeping the flag. Unset it; the production default is secure.
|
|
93
|
+
delete env.KESTREL_SECURE_COOKIES
|
|
94
|
+
|
|
95
|
+
const devPort = 41822
|
|
96
|
+
log('nuxt dev (brief boot, then kill) — schema auto-syncs additively at dev boot; a production boot never touches it (by design), so this is what a real `pnpm dev` first run also does')
|
|
97
|
+
const dev = spawn('npx', ['nuxt', 'dev', '--port', String(devPort)], {
|
|
98
|
+
cwd: consumerDir,
|
|
99
|
+
env: { ...process.env, ...env },
|
|
100
|
+
stdio: 'inherit',
|
|
101
|
+
})
|
|
102
|
+
try {
|
|
103
|
+
await waitForServer(`http://localhost:${devPort}/robots.txt`, 60000)
|
|
104
|
+
} finally {
|
|
105
|
+
dev.kill()
|
|
106
|
+
await sleep(1000) // let the dev Nitro process release the sqlite file before the production build reopens it
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log('nuxt build (production) — the real exports-map/deps resolution surface, not a dev-mode shortcut')
|
|
110
|
+
run('npx', ['nuxt', 'build'], { cwd: consumerDir, env: { ...process.env, ...env } })
|
|
111
|
+
|
|
112
|
+
log('booting the built server and hitting it over real HTTP')
|
|
113
|
+
const port = 41823
|
|
114
|
+
server = spawn('node', [join(consumerDir, '.output/server/index.mjs')], {
|
|
115
|
+
cwd: consumerDir,
|
|
116
|
+
env: { ...process.env, ...env, PORT: String(port), NITRO_PORT: String(port) },
|
|
117
|
+
stdio: 'inherit',
|
|
118
|
+
})
|
|
119
|
+
await waitForServer(`http://localhost:${port}/robots.txt`, 30000)
|
|
120
|
+
|
|
121
|
+
const checks = [
|
|
122
|
+
['GET /robots.txt', `http://localhost:${port}/robots.txt`, 200],
|
|
123
|
+
['GET /sitemap.xml', `http://localhost:${port}/sitemap.xml`, 200],
|
|
124
|
+
['GET /admin (login page, SPA shell)', `http://localhost:${port}/admin`, 200],
|
|
125
|
+
]
|
|
126
|
+
for (const [label, url, wantStatus] of checks) {
|
|
127
|
+
const res = await fetch(url)
|
|
128
|
+
if (res.status !== wantStatus) throw new Error(`${label} -> ${res.status}, expected ${wantStatus}`)
|
|
129
|
+
log(` ${label} -> ${res.status} OK`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const loginRes = await fetch(`http://localhost:${port}/api/login`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({ password }),
|
|
136
|
+
})
|
|
137
|
+
if (loginRes.status !== 200) throw new Error(`POST /api/login -> ${loginRes.status}, expected 200 (the packed admin auth stack did not resolve)`)
|
|
138
|
+
log(' POST /api/login -> 200 OK (the packed @michaelthielemann/kestrel-auth + @michaelthielemann/kestrel-access stack authenticates for real)')
|
|
139
|
+
|
|
140
|
+
log('PASS — a real npm install of the packed engine + packages boots and serves under production, and login round-trips through the packed auth stack.')
|
|
141
|
+
} finally {
|
|
142
|
+
server?.kill()
|
|
143
|
+
if (!process.env.KEEP_CONSUMER_TEMPLATE_CI_WORKDIR) rmSync(work, { recursive: true, force: true })
|
|
144
|
+
else log(`KEEP_CONSUMER_TEMPLATE_CI_WORKDIR set — leaving ${work} in place`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function waitForServer(url, timeoutMs) {
|
|
148
|
+
const deadline = Date.now() + timeoutMs
|
|
149
|
+
for (;;) {
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(url)
|
|
152
|
+
if (res.status) return
|
|
153
|
+
} catch { /* not up yet */ }
|
|
154
|
+
if (Date.now() > deadline) throw new Error(`server did not come up at ${url} within ${timeoutMs}ms`)
|
|
155
|
+
await sleep(250)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
const VITEST_BIN = path.join(ROOT, 'node_modules/.bin/vitest')
|
|
8
|
+
const PARTS_DIR = path.join(ROOT, 'reports/coverage/.parts')
|
|
9
|
+
const OUT_DIR = path.join(ROOT, 'reports/coverage')
|
|
10
|
+
|
|
11
|
+
function sameKeys(a, b) {
|
|
12
|
+
const ka = Object.keys(a)
|
|
13
|
+
const kb = Object.keys(b)
|
|
14
|
+
if (ka.length !== kb.length) return false
|
|
15
|
+
const setB = new Set(kb)
|
|
16
|
+
return ka.every((k) => setB.has(k))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mapsAgree(a, b) {
|
|
20
|
+
return sameKeys(a.statementMap, b.statementMap) && sameKeys(a.fnMap, b.fnMap) && sameKeys(a.branchMap, b.branchMap)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function countExecutedFunctions(entry) {
|
|
24
|
+
return Object.values(entry.f).filter((n) => n > 0).length
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cloneEntry(entry) {
|
|
28
|
+
return structuredClone(entry)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sumCounters(a, b) {
|
|
32
|
+
const out = {}
|
|
33
|
+
for (const k of Object.keys(a)) out[k] = a[k] + (b[k] ?? 0)
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sumBranches(a, b) {
|
|
38
|
+
const out = {}
|
|
39
|
+
for (const k of Object.keys(a)) {
|
|
40
|
+
const branchA = a[k]
|
|
41
|
+
const branchB = b[k] ?? []
|
|
42
|
+
out[k] = branchA.map((count, i) => count + (branchB[i] ?? 0))
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Istanbul JSON is keyed by absolute file path; the same path can show up in several parts
|
|
48
|
+
// (a package run and the root run both touching a shared file). When their instrumentation
|
|
49
|
+
// maps agree, counters are additive. When they disagree, the maps describe different
|
|
50
|
+
// transforms of the file — summing them would misattribute counts to the wrong statements/
|
|
51
|
+
// functions, so the richer entry wins outright and the conflict is reported instead of hidden.
|
|
52
|
+
export function mergeCoverage(parts, conflicts = []) {
|
|
53
|
+
const merged = {}
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
for (const [filePath, entry] of Object.entries(part)) {
|
|
56
|
+
const existing = merged[filePath]
|
|
57
|
+
if (!existing) {
|
|
58
|
+
merged[filePath] = cloneEntry(entry)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (mapsAgree(existing, entry)) {
|
|
62
|
+
merged[filePath] = {
|
|
63
|
+
...existing,
|
|
64
|
+
s: sumCounters(existing.s, entry.s),
|
|
65
|
+
f: sumCounters(existing.f, entry.f),
|
|
66
|
+
b: sumBranches(existing.b, entry.b),
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
if (countExecutedFunctions(entry) > countExecutedFunctions(existing)) {
|
|
70
|
+
merged[filePath] = cloneEntry(entry)
|
|
71
|
+
}
|
|
72
|
+
conflicts.push(filePath)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return merged
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readCoverage(dir) {
|
|
80
|
+
const file = path.join(dir, 'coverage-final.json')
|
|
81
|
+
if (!fs.existsSync(file)) return null
|
|
82
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function countFiles(coverage) {
|
|
86
|
+
return coverage ? Object.keys(coverage).length : 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function countFunctionsTotal(coverage) {
|
|
90
|
+
if (!coverage) return 0
|
|
91
|
+
let total = 0
|
|
92
|
+
for (const entry of Object.values(coverage)) total += countExecutedFunctions(entry)
|
|
93
|
+
return total
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoverPackageSuites() {
|
|
97
|
+
const packagesDir = path.join(ROOT, 'packages')
|
|
98
|
+
if (!fs.existsSync(packagesDir)) return []
|
|
99
|
+
return fs
|
|
100
|
+
.readdirSync(packagesDir)
|
|
101
|
+
.filter((name) => fs.existsSync(path.join(packagesDir, name, 'vitest.config.ts')))
|
|
102
|
+
.sort()
|
|
103
|
+
.map((name) => ({ name, cwd: path.join(packagesDir, name) }))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runSuite(name, cwd) {
|
|
107
|
+
const reportsDirectory = path.join(PARTS_DIR, name)
|
|
108
|
+
fs.mkdirSync(reportsDirectory, { recursive: true })
|
|
109
|
+
const args = [
|
|
110
|
+
'run',
|
|
111
|
+
'--coverage.enabled',
|
|
112
|
+
'--coverage.provider=v8',
|
|
113
|
+
'--coverage.reporter=json',
|
|
114
|
+
`--coverage.reportsDirectory=${reportsDirectory}`,
|
|
115
|
+
'--coverage.reportOnFailure=true',
|
|
116
|
+
]
|
|
117
|
+
console.log(`\n=== ${name} (${path.relative(ROOT, cwd) || '.'}) ===`)
|
|
118
|
+
const result = spawnSync(VITEST_BIN, args, { cwd, stdio: 'inherit' })
|
|
119
|
+
const passed = result.status === 0
|
|
120
|
+
const coverage = readCoverage(reportsDirectory)
|
|
121
|
+
return { name, passed, coverage }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function main() {
|
|
125
|
+
fs.rmSync(PARTS_DIR, { recursive: true, force: true })
|
|
126
|
+
fs.mkdirSync(PARTS_DIR, { recursive: true })
|
|
127
|
+
|
|
128
|
+
const suites = [{ name: 'root', cwd: ROOT }, ...discoverPackageSuites()]
|
|
129
|
+
const results = suites.map(({ name, cwd }) => runSuite(name, cwd))
|
|
130
|
+
|
|
131
|
+
const conflicts = []
|
|
132
|
+
const merged = mergeCoverage(
|
|
133
|
+
results.map((r) => r.coverage).filter(Boolean),
|
|
134
|
+
conflicts,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
fs.mkdirSync(OUT_DIR, { recursive: true })
|
|
138
|
+
fs.writeFileSync(path.join(OUT_DIR, 'coverage-final.json'), JSON.stringify(merged))
|
|
139
|
+
|
|
140
|
+
console.log('\n=== coverage-all summary ===')
|
|
141
|
+
for (const r of results) {
|
|
142
|
+
const status = r.passed ? 'PASS' : 'FAIL'
|
|
143
|
+
console.log(
|
|
144
|
+
`${r.name.padEnd(24)} ${status} files=${countFiles(r.coverage)} functionsExecuted=${countFunctionsTotal(r.coverage)}`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (conflicts.length > 0) {
|
|
149
|
+
console.log(`\n${conflicts.length} merge conflict(s) (kept the entry with more executed functions):`)
|
|
150
|
+
for (const filePath of conflicts) console.log(` ${filePath}`)
|
|
151
|
+
} else {
|
|
152
|
+
console.log('\nno merge conflicts')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
`\nmerged: files=${Object.keys(merged).length} functionsExecuted=${countFunctionsTotal(merged)} -> ${path.join(OUT_DIR, 'coverage-final.json')}`,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const anyFailed = results.some((r) => !r.passed)
|
|
160
|
+
process.exitCode = anyFailed ? 1 : 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (import.meta.url === `file://${process.argv[1]}`) main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Deterministic static dashboard generator — no network. Boots the pipeline/collection registries
|
|
2
|
+
// the same way test/architecture/pipeline-invariants.test.ts does (no Nuxt boot needed for that), then
|
|
3
|
+
// renders through the SAME renderDashboard() the dev route uses, so the two surfaces never drift.
|
|
4
|
+
//
|
|
5
|
+
// Run with: npx tsx scripts/dashboard.mjs (or `pnpm dashboard`)
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { join, dirname } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import Database from 'better-sqlite3'
|
|
11
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
12
|
+
import { fieldTypes } from '@michaelthielemann/kestrel-fields'
|
|
13
|
+
import {
|
|
14
|
+
buildIntrospectionPipelines, buildPipelineIndex, clearPipelines, clearRegistry,
|
|
15
|
+
registerCollection, registerPipeline, renderDashboard, setResolvedKestrelConfig, allCollections,
|
|
16
|
+
} from '@michaelthielemann/kestrel-core'
|
|
17
|
+
import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
|
|
18
|
+
import { pagesCollection } from '@michaelthielemann/kestrel-collections'
|
|
19
|
+
import { mediaCollection, mediaSettingsCollection } from '@michaelthielemann/kestrel-media'
|
|
20
|
+
import { redirectsCollection, siteCollection } from '@michaelthielemann/kestrel-publishing'
|
|
21
|
+
import { resolveServerKestrelConfig } from '../layers/core/server/utils/server-config.ts'
|
|
22
|
+
import { PLUGIN_ORDER } from '../layers/core/modules/plugin-order/plugin-order.ts'
|
|
23
|
+
import posts from '../server/collections/posts.ts'
|
|
24
|
+
import settings from '../server/collections/settings.ts'
|
|
25
|
+
|
|
26
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
27
|
+
|
|
28
|
+
void fieldTypes
|
|
29
|
+
setResolvedKestrelConfig(resolveServerKestrelConfig())
|
|
30
|
+
Object.assign(globalThis, {
|
|
31
|
+
useRuntimeConfig: () => ({
|
|
32
|
+
kestrel: { output: { driver: 'local', dir: '', publicDir: '/kestrel-no-such-public-dir', auto: true, publishOnSave: false, reconcileMinutes: 0, verbose: false, s3: {} } },
|
|
33
|
+
}),
|
|
34
|
+
useDb: () => drizzle(new Database(':memory:')),
|
|
35
|
+
primaryLocale: () => 'en',
|
|
36
|
+
prefixPrimaryLocale: () => false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
clearRegistry()
|
|
40
|
+
for (const c of [pagesCollection, posts, settings, mediaCollection, mediaSettingsCollection, redirectsCollection, siteCollection]) registerCollection(c)
|
|
41
|
+
|
|
42
|
+
clearPipelines()
|
|
43
|
+
for (const def of buildAuthPipelines()) registerPipeline(def)
|
|
44
|
+
for (const def of buildIntrospectionPipelines()) registerPipeline(def)
|
|
45
|
+
|
|
46
|
+
const collections = allCollections().map((c) => ({
|
|
47
|
+
name: c.name,
|
|
48
|
+
mode: c.def.mode,
|
|
49
|
+
translatable: Boolean(c.def.translatable),
|
|
50
|
+
fieldCount: Object.keys(c.def.fields).length,
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
/** Reads a repo-only enrichment file, or returns undefined — a consumer project (or a fresh checkout
|
|
54
|
+
* before `graphify update`/`pnpm api:check` have run) ships none of these, so every read is optional. */
|
|
55
|
+
function readJsonIfExists(relPath) {
|
|
56
|
+
const abs = join(repoRoot, relPath)
|
|
57
|
+
if (!existsSync(abs)) return undefined
|
|
58
|
+
return JSON.parse(readFileSync(abs, 'utf8'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function apiSurfaceSection() {
|
|
62
|
+
const ceilings = readJsonIfExists('test/architecture/public-api-ceilings.json')
|
|
63
|
+
if (!ceilings) return undefined
|
|
64
|
+
const out = []
|
|
65
|
+
for (const [report, ceiling] of Object.entries(ceilings)) {
|
|
66
|
+
const abs = join(repoRoot, report)
|
|
67
|
+
if (!existsSync(abs)) continue
|
|
68
|
+
const count = readFileSync(abs, 'utf8').split('\n').filter((l) => /^\/\/ @(public|alpha|beta|internal)\b/.test(l)).length
|
|
69
|
+
out.push({ report, count, ceiling })
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const edgeAllowlist = readJsonIfExists('test/architecture/edge-allowlist.json')
|
|
75
|
+
const apiSurface = apiSurfaceSection()
|
|
76
|
+
|
|
77
|
+
const html = renderDashboard({
|
|
78
|
+
pipelines: buildPipelineIndex(),
|
|
79
|
+
pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
|
|
80
|
+
collections,
|
|
81
|
+
graph: (edgeAllowlist || apiSurface) ? { packageEdges: edgeAllowlist?.edges, apiSurface } : undefined,
|
|
82
|
+
generatedAt: new Date().toISOString(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const outDir = join(repoRoot, 'docs')
|
|
86
|
+
mkdirSync(outDir, { recursive: true })
|
|
87
|
+
const outFile = join(outDir, 'dashboard.html')
|
|
88
|
+
writeFileSync(outFile, html)
|
|
89
|
+
console.log(`[dashboard] wrote ${outFile} (${buildPipelineIndex().length} pipelines, ${collections.length} collections)`)
|
|
@@ -3,7 +3,7 @@ import { hashPassword } from './lib/password.mjs'
|
|
|
3
3
|
import { Cancelled, promptPassword } from './lib/cli.mjs'
|
|
4
4
|
|
|
5
5
|
// Not used at runtime: this prints a value an operator pastes into KESTREL_ADMIN_PASSWORD_HASH. Kept as a
|
|
6
|
-
// standalone entry point because docs/
|
|
6
|
+
// standalone entry point because docs/guide/getting-started.md tells consumers to run it straight out of
|
|
7
7
|
// node_modules, which must keep working whether or not the `kestrel` bin is on PATH.
|
|
8
8
|
const arg = process.argv[2]
|
|
9
9
|
if (arg) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Indexed view over an Istanbul-shaped coverage report, for the audit detectors.
|
|
2
|
+
//
|
|
3
|
+
// Two questions only: which branch arms were never taken, and which functions never ran. Both are
|
|
4
|
+
// keyed by line so they can be joined to the graph, whose nodes carry a start line and no span.
|
|
5
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
6
|
+
import { join, relative, isAbsolute } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export function loadCoverage(root, file = 'reports/coverage/coverage-final.json') {
|
|
9
|
+
const path = isAbsolute(file) ? file : join(root, file)
|
|
10
|
+
let raw
|
|
11
|
+
let generatedAt
|
|
12
|
+
try {
|
|
13
|
+
raw = JSON.parse(readFileSync(path, 'utf8'))
|
|
14
|
+
generatedAt = statSync(path).mtimeMs
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const byFile = new Map()
|
|
20
|
+
for (const entry of Object.values(raw)) {
|
|
21
|
+
const rel = relative(root, entry.path)
|
|
22
|
+
const missedBranchLines = new Set()
|
|
23
|
+
let branchMissCount = 0
|
|
24
|
+
for (const [key, arm] of Object.entries(entry.branchMap ?? {})) {
|
|
25
|
+
const counts = entry.b?.[key] ?? []
|
|
26
|
+
arm.locations?.forEach((location, index) => {
|
|
27
|
+
if (counts[index] !== 0) return
|
|
28
|
+
branchMissCount += 1
|
|
29
|
+
const line = location.start?.line ?? arm.line
|
|
30
|
+
if (line) missedBranchLines.add(line)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
const ranFunctionLines = new Set()
|
|
34
|
+
for (const [key, fn] of Object.entries(entry.fnMap ?? {})) {
|
|
35
|
+
if (!entry.f?.[key]) continue
|
|
36
|
+
const line = fn.decl?.start?.line ?? fn.loc?.start?.line
|
|
37
|
+
if (line) ranFunctionLines.add(line)
|
|
38
|
+
}
|
|
39
|
+
byFile.set(rel, { missedBranchLines, branchMissCount, ranFunctionLines })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const empty = { missedBranchLines: new Set(), branchMissCount: 0, ranFunctionLines: new Set() }
|
|
43
|
+
return {
|
|
44
|
+
generatedAt,
|
|
45
|
+
files: [...byFile.keys()],
|
|
46
|
+
has: (file) => byFile.has(file),
|
|
47
|
+
missedBranchLines: (file) => (byFile.get(file) ?? empty).missedBranchLines,
|
|
48
|
+
branchMissCount: (file) => (byFile.get(file) ?? empty).branchMissCount,
|
|
49
|
+
ranFunctionLines: (file) => (byFile.get(file) ?? empty).ranFunctionLines,
|
|
50
|
+
}
|
|
51
|
+
}
|