@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
package/dist/load.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { readdir, stat } from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { isAbsolute, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { KestrelBootError } from "./errors.js";
|
|
14
|
+
async function importDefault(file, owner) {
|
|
15
|
+
let mod;
|
|
16
|
+
try {
|
|
17
|
+
mod = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
throw new KestrelBootError(owner, `cannot load ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
21
|
+
}
|
|
22
|
+
const def = mod.default;
|
|
23
|
+
if (def === undefined)
|
|
24
|
+
throw new KestrelBootError(owner, `${file} has no default export`);
|
|
25
|
+
return def;
|
|
26
|
+
}
|
|
27
|
+
export async function loadConfig(file) {
|
|
28
|
+
const def = await importDefault(file, "kestrel.config");
|
|
29
|
+
if (typeof def !== "object" || def === null)
|
|
30
|
+
throw new KestrelBootError("kestrel.config", "default export is not an object");
|
|
31
|
+
return def;
|
|
32
|
+
}
|
|
33
|
+
export function resolveModuleFile(root, use) {
|
|
34
|
+
if (use.startsWith("./") || use.startsWith("../") || isAbsolute(use))
|
|
35
|
+
return resolve(root, use);
|
|
36
|
+
return createRequire(pathToFileURL(resolve(root, "package.json")).href).resolve(use);
|
|
37
|
+
}
|
|
38
|
+
export async function loadModules(root, config) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const entry of config.modules) {
|
|
41
|
+
let file;
|
|
42
|
+
try {
|
|
43
|
+
file = resolveModuleFile(root, entry.use);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new KestrelBootError(entry.use, `cannot resolve module: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
}
|
|
48
|
+
const def = await importDefault(file, entry.use);
|
|
49
|
+
const m = def;
|
|
50
|
+
if (typeof m !== "object" || m === null || typeof m.name !== "string" || typeof m.setup !== "function" || !Array.isArray(m.provides) || !Array.isArray(m.requires) || m.configSchema === undefined) {
|
|
51
|
+
throw new KestrelBootError(entry.use, "default export is not a defineModule() result");
|
|
52
|
+
}
|
|
53
|
+
out.push(m);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export async function loadPipelines(root, dir) {
|
|
58
|
+
const abs = resolve(root, dir);
|
|
59
|
+
try {
|
|
60
|
+
if (!(await stat(abs)).isDirectory())
|
|
61
|
+
throw new KestrelBootError("pipelines", `${abs} is not a directory`);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.code === "ENOENT")
|
|
65
|
+
return [];
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const files = (await readdir(abs)).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")).sort();
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const def = await importDefault(resolve(abs, file), `pipelines/${file}`);
|
|
72
|
+
const p = def;
|
|
73
|
+
if (typeof p !== "object" || p === null || typeof p.name !== "string" || !Array.isArray(p.steps)) {
|
|
74
|
+
throw new KestrelBootError(`pipelines/${file}`, "default export is not a definePipeline() result");
|
|
75
|
+
}
|
|
76
|
+
out.push(p);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StepLog {
|
|
2
|
+
runId: string;
|
|
3
|
+
parentRunId?: string;
|
|
4
|
+
pipeline: string;
|
|
5
|
+
step: string;
|
|
6
|
+
ms: number;
|
|
7
|
+
outcome: "ok" | `fail(${string})` | "error";
|
|
8
|
+
}
|
|
9
|
+
export interface Logger {
|
|
10
|
+
step(entry: StepLog): void;
|
|
11
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
12
|
+
warn?(message: string, data?: Record<string, unknown>): void;
|
|
13
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function logWarn(logger: Logger, message: string, data?: Record<string, unknown>): void;
|
|
16
|
+
export declare function localIso(date: Date): string;
|
|
17
|
+
export declare const consoleLogger: Logger;
|
|
18
|
+
export declare const silentLogger: Logger;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function logWarn(logger, message, data) {
|
|
2
|
+
if (logger.warn)
|
|
3
|
+
logger.warn(message, data);
|
|
4
|
+
else
|
|
5
|
+
logger.info(message, data);
|
|
6
|
+
}
|
|
7
|
+
// ISO 8601 with the local UTC offset (not "Z"), since consoleLogger runs on one machine
|
|
8
|
+
// and log lines should read in local time without losing offset information.
|
|
9
|
+
export function localIso(date) {
|
|
10
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
11
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
12
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
13
|
+
const abs = Math.abs(offsetMinutes);
|
|
14
|
+
const offset = `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
15
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}${offset}`;
|
|
16
|
+
}
|
|
17
|
+
export const consoleLogger = {
|
|
18
|
+
step(entry) {
|
|
19
|
+
console.log(JSON.stringify({ time: localIso(new Date()), level: "step", ...entry }));
|
|
20
|
+
},
|
|
21
|
+
info(message, data = {}) {
|
|
22
|
+
console.log(JSON.stringify({ time: localIso(new Date()), level: "info", message, ...data }));
|
|
23
|
+
},
|
|
24
|
+
warn(message, data = {}) {
|
|
25
|
+
console.warn(JSON.stringify({ time: localIso(new Date()), level: "warn", message, ...data }));
|
|
26
|
+
},
|
|
27
|
+
error(message, data = {}) {
|
|
28
|
+
console.error(JSON.stringify({ time: localIso(new Date()), level: "error", message, ...data }));
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const silentLogger = {
|
|
32
|
+
step() { },
|
|
33
|
+
info() { },
|
|
34
|
+
warn() { },
|
|
35
|
+
error() { },
|
|
36
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Step, type StepMap } from "./context.ts";
|
|
2
|
+
import type { StepDescription, StepDescriptions } from "./defineModule.ts";
|
|
3
|
+
export interface ResolvedStep {
|
|
4
|
+
name: string;
|
|
5
|
+
fn: Step;
|
|
6
|
+
description: StepDescription;
|
|
7
|
+
}
|
|
8
|
+
export declare class StepRegistry {
|
|
9
|
+
private readonly entries;
|
|
10
|
+
register(owner: string, prefix: string, steps: StepMap, descriptions?: StepDescriptions): void;
|
|
11
|
+
has(name: string): boolean;
|
|
12
|
+
names(): string[];
|
|
13
|
+
resolve(spec: string, owner?: string): ResolvedStep;
|
|
14
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isStepFactory } from "./context.js";
|
|
2
|
+
import { KestrelBootError } from "./errors.js";
|
|
3
|
+
const CONTEXT_PATH = /^[a-zA-Z][A-Za-z0-9]*(\.[A-Za-z0-9_-]+)*\??$/;
|
|
4
|
+
// Walks the prototype chain (like `missingMethods` in defineContract.ts, via
|
|
5
|
+
// property access) so a class-instance step map registers its inherited methods too.
|
|
6
|
+
function stepKeys(steps) {
|
|
7
|
+
const keys = new Set();
|
|
8
|
+
for (let obj = steps; obj && obj !== Object.prototype; obj = Object.getPrototypeOf(obj)) {
|
|
9
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
10
|
+
if (key !== "constructor")
|
|
11
|
+
keys.add(key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return [...keys];
|
|
15
|
+
}
|
|
16
|
+
function checkPaths(owner, name, description) {
|
|
17
|
+
for (const path of description.reads) {
|
|
18
|
+
if (path.endsWith("?"))
|
|
19
|
+
throw new KestrelBootError(owner, `step "${name}" reads "${path}"; the trailing "?" is allowed in writes only`);
|
|
20
|
+
if (!CONTEXT_PATH.test(path))
|
|
21
|
+
throw new KestrelBootError(owner, `step "${name}" declares an invalid read path "${path}"`);
|
|
22
|
+
}
|
|
23
|
+
for (const path of description.writes) {
|
|
24
|
+
if (!CONTEXT_PATH.test(path))
|
|
25
|
+
throw new KestrelBootError(owner, `step "${name}" declares an invalid write path "${path}"`);
|
|
26
|
+
}
|
|
27
|
+
return description;
|
|
28
|
+
}
|
|
29
|
+
export class StepRegistry {
|
|
30
|
+
entries = new Map();
|
|
31
|
+
register(owner, prefix, steps, descriptions = {}) {
|
|
32
|
+
for (const key of stepKeys(steps)) {
|
|
33
|
+
const value = steps[key];
|
|
34
|
+
const name = `${prefix}.${key}`;
|
|
35
|
+
if (typeof value !== "function")
|
|
36
|
+
throw new KestrelBootError(owner, `step "${name}" is not a function`);
|
|
37
|
+
const fn = value;
|
|
38
|
+
const existing = this.entries.get(name);
|
|
39
|
+
if (existing)
|
|
40
|
+
throw new KestrelBootError(owner, `step "${name}" is already registered by ${existing.owner}`);
|
|
41
|
+
const describe = descriptions[key];
|
|
42
|
+
if (describe === undefined)
|
|
43
|
+
throw new KestrelBootError(owner, `step "${name}" has no describe() entry`);
|
|
44
|
+
if (typeof describe === "function") {
|
|
45
|
+
if (!isStepFactory(fn))
|
|
46
|
+
throw new KestrelBootError(owner, `step "${name}" takes no argument but its describe() entry is a function`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
checkPaths(owner, name, describe);
|
|
50
|
+
}
|
|
51
|
+
this.entries.set(name, { owner, fn, describe });
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(descriptions)) {
|
|
54
|
+
if (!(key in steps))
|
|
55
|
+
throw new KestrelBootError(owner, `describe() names step "${prefix}.${key}" which does not exist`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
has(name) {
|
|
59
|
+
return this.entries.has(name);
|
|
60
|
+
}
|
|
61
|
+
names() {
|
|
62
|
+
return [...this.entries.keys()];
|
|
63
|
+
}
|
|
64
|
+
resolve(spec, owner = "pipeline") {
|
|
65
|
+
const colon = spec.indexOf(":");
|
|
66
|
+
const name = colon === -1 ? spec : spec.slice(0, colon);
|
|
67
|
+
const arg = colon === -1 ? undefined : spec.slice(colon + 1);
|
|
68
|
+
const entry = this.entries.get(name);
|
|
69
|
+
if (!entry)
|
|
70
|
+
throw new KestrelBootError(owner, `unknown step "${name}"`);
|
|
71
|
+
const factory = isStepFactory(entry.fn);
|
|
72
|
+
if (arg === undefined) {
|
|
73
|
+
if (factory)
|
|
74
|
+
throw new KestrelBootError(owner, `step "${name}" requires an argument`);
|
|
75
|
+
return { name: spec, fn: entry.fn, description: entry.describe };
|
|
76
|
+
}
|
|
77
|
+
if (arg === "")
|
|
78
|
+
throw new KestrelBootError(owner, `step "${name}" has an empty argument`);
|
|
79
|
+
if (!factory)
|
|
80
|
+
throw new KestrelBootError(owner, `step "${name}" does not accept an argument`);
|
|
81
|
+
const built = entry.fn(arg);
|
|
82
|
+
if (typeof built !== "function")
|
|
83
|
+
throw new KestrelBootError(owner, `step "${name}" returned ${typeof built} instead of a step`);
|
|
84
|
+
const description = typeof entry.describe === "function" ? checkPaths(entry.owner, spec, entry.describe(arg)) : entry.describe;
|
|
85
|
+
return { name: spec, fn: built, description };
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/result.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Ok<T> {
|
|
2
|
+
readonly ok: true;
|
|
3
|
+
readonly value: T;
|
|
4
|
+
}
|
|
5
|
+
export interface Err<E> {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly error: E;
|
|
8
|
+
}
|
|
9
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
|
10
|
+
export declare function ok(): Ok<void>;
|
|
11
|
+
export declare function ok<T>(value: T): Ok<T>;
|
|
12
|
+
export declare function err<E>(error: E): Err<E>;
|
|
13
|
+
export declare function isOk<T, E>(result: Result<T, E>): result is Ok<T>;
|
|
14
|
+
export declare function isErr<T, E>(result: Result<T, E>): result is Err<E>;
|
|
15
|
+
export declare function match<T, E, A, B>(result: Result<T, E>, on: {
|
|
16
|
+
ok: (value: T) => A;
|
|
17
|
+
err: (error: E) => B;
|
|
18
|
+
}): A | B;
|
|
19
|
+
export declare function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T;
|
package/dist/result.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function ok(value) {
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
3
|
+
return { ok: true, value: value };
|
|
4
|
+
}
|
|
5
|
+
export function err(error) {
|
|
6
|
+
return { ok: false, error };
|
|
7
|
+
}
|
|
8
|
+
export function isOk(result) {
|
|
9
|
+
return result.ok;
|
|
10
|
+
}
|
|
11
|
+
export function isErr(result) {
|
|
12
|
+
return !result.ok;
|
|
13
|
+
}
|
|
14
|
+
export function match(result, on) {
|
|
15
|
+
return result.ok ? on.ok(result.value) : on.err(result.error);
|
|
16
|
+
}
|
|
17
|
+
export function unwrapOr(result, fallback) {
|
|
18
|
+
return result.ok ? result.value : fallback;
|
|
19
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ContextInput } from "./context.ts";
|
|
2
|
+
import type { Logger } from "./logger.ts";
|
|
3
|
+
import type { ResolvedStep } from "./registry.ts";
|
|
4
|
+
export interface ResolvedPipeline {
|
|
5
|
+
name: string;
|
|
6
|
+
steps: readonly ResolvedStep[];
|
|
7
|
+
}
|
|
8
|
+
export interface RunResult {
|
|
9
|
+
runId: string;
|
|
10
|
+
requestId?: string;
|
|
11
|
+
status: number;
|
|
12
|
+
result?: unknown;
|
|
13
|
+
error?: string;
|
|
14
|
+
code?: string;
|
|
15
|
+
retryable?: boolean;
|
|
16
|
+
step?: string;
|
|
17
|
+
details?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export type Runner = (pipeline: string, input: ContextInput) => Promise<RunResult>;
|
|
20
|
+
export interface RunOptions {
|
|
21
|
+
validateInput: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface RunTracker {
|
|
24
|
+
track<T>(fn: () => Promise<T>): Promise<T>;
|
|
25
|
+
active(): number;
|
|
26
|
+
drain(timeoutMs: number): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function createRunTracker(): RunTracker;
|
|
29
|
+
export declare function runPipeline(pipeline: ResolvedPipeline, input: ContextInput, logger: Logger, options?: RunOptions): Promise<RunResult>;
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createContext, DONE } from "./context.js";
|
|
3
|
+
import { failure, isKestrelError } from "./errors.js";
|
|
4
|
+
import { isErr } from "./result.js";
|
|
5
|
+
import { validateSchema } from "./schema.js";
|
|
6
|
+
export function createRunTracker() {
|
|
7
|
+
let active = 0;
|
|
8
|
+
const idle = new Set();
|
|
9
|
+
return {
|
|
10
|
+
async track(fn) {
|
|
11
|
+
active++;
|
|
12
|
+
try {
|
|
13
|
+
return await fn();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
active--;
|
|
17
|
+
if (active === 0)
|
|
18
|
+
for (const resolve of [...idle])
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
active: () => active,
|
|
23
|
+
drain(timeoutMs) {
|
|
24
|
+
if (active === 0)
|
|
25
|
+
return Promise.resolve(true);
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const onIdle = () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
idle.delete(onIdle);
|
|
30
|
+
resolve(true);
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
idle.delete(onIdle);
|
|
34
|
+
resolve(false);
|
|
35
|
+
}, Math.max(0, timeoutMs));
|
|
36
|
+
timer.unref();
|
|
37
|
+
idle.add(onIdle);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isResult(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null)
|
|
44
|
+
return false;
|
|
45
|
+
const result = value;
|
|
46
|
+
if (result.ok === true)
|
|
47
|
+
return "value" in result;
|
|
48
|
+
return result.ok === false && isKestrelError(result.error);
|
|
49
|
+
}
|
|
50
|
+
function isContext(value) {
|
|
51
|
+
if (typeof value !== "object" || value === null)
|
|
52
|
+
return false;
|
|
53
|
+
const ctx = value;
|
|
54
|
+
const record = (v) => typeof v === "object" && v !== null;
|
|
55
|
+
return record(ctx.trigger) && record(ctx.payload) && record(ctx.params) && record(ctx.headers) && Array.isArray(ctx.files) && typeof ctx.fail === "function" && typeof ctx.done === "function";
|
|
56
|
+
}
|
|
57
|
+
// Query parameters are merged into the payload by the HTTP trigger, so an `additionalProperties:
|
|
58
|
+
// false` input schema has to accept them too.
|
|
59
|
+
function payloadSchema(description) {
|
|
60
|
+
const input = description.input;
|
|
61
|
+
if (input === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (description.query === undefined)
|
|
64
|
+
return input;
|
|
65
|
+
const properties = typeof input.properties === "object" && input.properties !== null ? input.properties : {};
|
|
66
|
+
return { ...input, properties: { ...properties, ...description.query } };
|
|
67
|
+
}
|
|
68
|
+
export async function runPipeline(pipeline, input, logger, options = { validateInput: false }) {
|
|
69
|
+
const runId = randomUUID();
|
|
70
|
+
let ctx = createContext(input, runId);
|
|
71
|
+
const requestId = ctx.requestId;
|
|
72
|
+
const trace = (result) => (requestId === undefined ? result : { ...result, requestId });
|
|
73
|
+
for (const step of pipeline.steps) {
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
const log = (outcome) => logger.step({
|
|
76
|
+
runId,
|
|
77
|
+
...(input.parentRunId === undefined ? {} : { parentRunId: input.parentRunId }),
|
|
78
|
+
pipeline: pipeline.name,
|
|
79
|
+
step: step.name,
|
|
80
|
+
ms: Math.round((performance.now() - started) * 100) / 100,
|
|
81
|
+
outcome,
|
|
82
|
+
});
|
|
83
|
+
if (options.validateInput) {
|
|
84
|
+
const schema = payloadSchema(step.description);
|
|
85
|
+
const problem = schema === undefined ? undefined : validateSchema(schema, ctx.payload)[0];
|
|
86
|
+
if (problem !== undefined) {
|
|
87
|
+
log("error");
|
|
88
|
+
const error = failure("INTERNAL", `dev: payload of step "${step.name}" in pipeline "${pipeline.name}" does not match describe().input at ${problem.path}: ${problem.message}`);
|
|
89
|
+
return trace({ runId, status: error.status, error: error.message, code: error.code, retryable: error.retryable, step: step.name });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const out = await step.fn(ctx);
|
|
94
|
+
if (!isResult(out)) {
|
|
95
|
+
throw new Error(`step "${step.name}" returned ${typeof out} instead of a Result<Context>`);
|
|
96
|
+
}
|
|
97
|
+
if (isErr(out)) {
|
|
98
|
+
const error = out.error;
|
|
99
|
+
log(`fail(${error.code})`);
|
|
100
|
+
if (error.cause !== undefined) {
|
|
101
|
+
const cause = error.cause instanceof Error ? { stack: error.cause.stack ?? error.cause.message } : { cause: error.cause };
|
|
102
|
+
logger.error(`pipeline "${pipeline.name}" step "${step.name}" failed with a cause`, { runId, code: error.code, ...cause });
|
|
103
|
+
}
|
|
104
|
+
return trace({
|
|
105
|
+
runId,
|
|
106
|
+
status: error.status,
|
|
107
|
+
error: error.message,
|
|
108
|
+
code: error.code,
|
|
109
|
+
retryable: error.retryable,
|
|
110
|
+
step: step.name,
|
|
111
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!isContext(out.value)) {
|
|
115
|
+
throw new Error(`step "${step.name}" returned ${typeof out.value} instead of a Result<Context>`);
|
|
116
|
+
}
|
|
117
|
+
ctx = Object.freeze(out.value);
|
|
118
|
+
log("ok");
|
|
119
|
+
if (Reflect.get(ctx, DONE) === true)
|
|
120
|
+
return trace({ runId, status: 200, result: ctx.result });
|
|
121
|
+
}
|
|
122
|
+
catch (thrown) {
|
|
123
|
+
log("error");
|
|
124
|
+
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
125
|
+
logger.error(`pipeline "${pipeline.name}" step "${step.name}" threw`, { runId, stack: error.stack ?? error.message });
|
|
126
|
+
return trace({ runId, status: 500, error: `${pipeline.name}/${step.name}: ${error.message}`, code: "INTERNAL", retryable: false, step: step.name });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const done = { runId, status: 200 };
|
|
130
|
+
if (ctx.result !== undefined)
|
|
131
|
+
done.result = ctx.result;
|
|
132
|
+
return trace(done);
|
|
133
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonSchema } from "./defineModule.ts";
|
|
2
|
+
export interface SchemaProblem {
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
/** The subset of JSON Schema the shipped `describe().input` blocks use; unknown keywords are ignored. */
|
|
7
|
+
export declare function validateSchema(schema: JsonSchema, value: unknown): SchemaProblem[];
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const ROOT = "$";
|
|
2
|
+
function child(path, key) {
|
|
3
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function subSchema(value) {
|
|
9
|
+
return isRecord(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function jsonType(value) {
|
|
12
|
+
if (value === null)
|
|
13
|
+
return "null";
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return "array";
|
|
16
|
+
return typeof value;
|
|
17
|
+
}
|
|
18
|
+
function matchesType(value, type) {
|
|
19
|
+
if (type === "integer")
|
|
20
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
21
|
+
if (type === "number")
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
23
|
+
if (type === "null")
|
|
24
|
+
return value === null;
|
|
25
|
+
if (type === "array")
|
|
26
|
+
return Array.isArray(value);
|
|
27
|
+
if (type === "object")
|
|
28
|
+
return isRecord(value);
|
|
29
|
+
return jsonType(value) === type;
|
|
30
|
+
}
|
|
31
|
+
function typeNames(type) {
|
|
32
|
+
if (typeof type === "string")
|
|
33
|
+
return [type];
|
|
34
|
+
if (Array.isArray(type))
|
|
35
|
+
return type.filter((t) => typeof t === "string");
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
function deepEqual(a, b) {
|
|
39
|
+
if (a === b)
|
|
40
|
+
return true;
|
|
41
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
42
|
+
return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
43
|
+
if (isRecord(a) && isRecord(b)) {
|
|
44
|
+
const keys = Object.keys(a);
|
|
45
|
+
return keys.length === Object.keys(b).length && keys.every((k) => Object.hasOwn(b, k) && deepEqual(a[k], b[k]));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function present(object, key) {
|
|
50
|
+
return Object.hasOwn(object, key) && object[key] !== undefined;
|
|
51
|
+
}
|
|
52
|
+
function check(schema, value, path, problems) {
|
|
53
|
+
const types = typeNames(schema.type);
|
|
54
|
+
if (types.length > 0 && !types.some((t) => matchesType(value, t))) {
|
|
55
|
+
problems.push({ path, message: `expected ${types.join(" or ")}, got ${jsonType(value)}` });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((allowed) => deepEqual(allowed, value))) {
|
|
59
|
+
problems.push({ path, message: `expected one of ${schema.enum.map((v) => JSON.stringify(v)).join(", ")}` });
|
|
60
|
+
}
|
|
61
|
+
if (Object.hasOwn(schema, "const") && !deepEqual(schema.const, value)) {
|
|
62
|
+
problems.push({ path, message: `expected ${JSON.stringify(schema.const)}` });
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
const length = [...value].length;
|
|
66
|
+
if (typeof schema.pattern === "string" && !new RegExp(schema.pattern).test(value)) {
|
|
67
|
+
problems.push({ path, message: `does not match ${schema.pattern}` });
|
|
68
|
+
}
|
|
69
|
+
if (typeof schema.minLength === "number" && length < schema.minLength) {
|
|
70
|
+
problems.push({ path, message: `shorter than minLength ${schema.minLength}` });
|
|
71
|
+
}
|
|
72
|
+
if (typeof schema.maxLength === "number" && length > schema.maxLength) {
|
|
73
|
+
problems.push({ path, message: `longer than maxLength ${schema.maxLength}` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "number") {
|
|
77
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
78
|
+
problems.push({ path, message: `below minimum ${schema.minimum}` });
|
|
79
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
80
|
+
problems.push({ path, message: `above maximum ${schema.maximum}` });
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
84
|
+
problems.push({ path, message: `fewer than minItems ${schema.minItems}` });
|
|
85
|
+
}
|
|
86
|
+
const items = schema.items;
|
|
87
|
+
if (Array.isArray(items)) {
|
|
88
|
+
items.forEach((item, i) => {
|
|
89
|
+
const sub = subSchema(item);
|
|
90
|
+
if (sub && i < value.length)
|
|
91
|
+
check(sub, value[i], `${path}[${i}]`, problems);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const sub = subSchema(items);
|
|
96
|
+
if (sub)
|
|
97
|
+
value.forEach((item, i) => check(sub, item, `${path}[${i}]`, problems));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (isRecord(value)) {
|
|
101
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
102
|
+
if (Array.isArray(schema.required)) {
|
|
103
|
+
for (const key of schema.required) {
|
|
104
|
+
if (typeof key === "string" && !present(value, key))
|
|
105
|
+
problems.push({ path: child(path, key), message: "is required" });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const [key, raw] of Object.entries(properties)) {
|
|
109
|
+
const sub = subSchema(raw);
|
|
110
|
+
if (sub && present(value, key))
|
|
111
|
+
check(sub, value[key], child(path, key), problems);
|
|
112
|
+
}
|
|
113
|
+
if (schema.additionalProperties === false) {
|
|
114
|
+
for (const key of Object.keys(value)) {
|
|
115
|
+
if (!Object.hasOwn(properties, key))
|
|
116
|
+
problems.push({ path: child(path, key), message: "is not allowed by additionalProperties: false" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(schema.oneOf)) {
|
|
121
|
+
const matched = schema.oneOf.filter((raw) => {
|
|
122
|
+
const sub = subSchema(raw);
|
|
123
|
+
return sub !== undefined && validateSchema(sub, value).length === 0;
|
|
124
|
+
}).length;
|
|
125
|
+
if (matched !== 1)
|
|
126
|
+
problems.push({ path, message: `matches ${String(matched)} of ${String(schema.oneOf.length)} oneOf schemas, expected exactly 1` });
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(schema.anyOf)) {
|
|
129
|
+
const matched = schema.anyOf.some((raw) => {
|
|
130
|
+
const sub = subSchema(raw);
|
|
131
|
+
return sub !== undefined && validateSchema(sub, value).length === 0;
|
|
132
|
+
});
|
|
133
|
+
if (!matched)
|
|
134
|
+
problems.push({ path, message: `matches none of the ${String(schema.anyOf.length)} anyOf schemas` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The subset of JSON Schema the shipped `describe().input` blocks use; unknown keywords are ignored. */
|
|
138
|
+
export function validateSchema(schema, value) {
|
|
139
|
+
const problems = [];
|
|
140
|
+
check(schema, value, ROOT, problems);
|
|
141
|
+
return problems;
|
|
142
|
+
}
|
package/dist/sort.d.ts
ADDED
package/dist/sort.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { KestrelBootError } from "./errors.js";
|
|
2
|
+
export function sortModules(modules) {
|
|
3
|
+
const providerOf = new Map();
|
|
4
|
+
for (const m of modules) {
|
|
5
|
+
for (const contract of m.provides) {
|
|
6
|
+
const other = providerOf.get(contract.name);
|
|
7
|
+
if (other)
|
|
8
|
+
throw new KestrelBootError(m.name, `contract "${contract.name}" is already provided by ${other.name}`);
|
|
9
|
+
providerOf.set(contract.name, m);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const dependsOn = new Map();
|
|
13
|
+
for (const m of modules) {
|
|
14
|
+
const deps = [];
|
|
15
|
+
for (const contract of m.requires) {
|
|
16
|
+
const provider = providerOf.get(contract.name);
|
|
17
|
+
if (!provider)
|
|
18
|
+
throw new KestrelBootError(m.name, `requires "${contract.name}" but no active module provides it`);
|
|
19
|
+
if (provider === m)
|
|
20
|
+
throw new KestrelBootError(m.name, `requires "${contract.name}" which it provides itself`);
|
|
21
|
+
deps.push(provider);
|
|
22
|
+
}
|
|
23
|
+
for (const contract of m.optional ?? []) {
|
|
24
|
+
const provider = providerOf.get(contract.name);
|
|
25
|
+
if (!provider)
|
|
26
|
+
continue;
|
|
27
|
+
if (provider === m)
|
|
28
|
+
throw new KestrelBootError(m.name, `optional "${contract.name}" which it provides itself`);
|
|
29
|
+
deps.push(provider);
|
|
30
|
+
}
|
|
31
|
+
dependsOn.set(m, deps);
|
|
32
|
+
}
|
|
33
|
+
const sorted = [];
|
|
34
|
+
const state = new Map();
|
|
35
|
+
const visit = (m, path) => {
|
|
36
|
+
const s = state.get(m);
|
|
37
|
+
if (s === "done")
|
|
38
|
+
return;
|
|
39
|
+
if (s === "visiting")
|
|
40
|
+
throw new KestrelBootError(m.name, `dependency cycle: ${[...path, m.name].join(" -> ")}`);
|
|
41
|
+
state.set(m, "visiting");
|
|
42
|
+
for (const dep of dependsOn.get(m) ?? [])
|
|
43
|
+
visit(dep, [...path, m.name]);
|
|
44
|
+
state.set(m, "done");
|
|
45
|
+
sorted.push(m);
|
|
46
|
+
};
|
|
47
|
+
for (const m of modules)
|
|
48
|
+
visit(m, []);
|
|
49
|
+
return sorted;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContextInput, Step, StepFactory } from "../context.ts";
|
|
2
|
+
import type { PipelineDefinition } from "../definePipeline.ts";
|
|
3
|
+
import { type Logger } from "../logger.ts";
|
|
4
|
+
import { type RunResult } from "../runner.ts";
|
|
5
|
+
export interface RunPipelineOptions {
|
|
6
|
+
steps: Record<string, Step | StepFactory>;
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
}
|
|
9
|
+
export declare function runPipeline(pipeline: PipelineDefinition, input: Partial<ContextInput>, options: RunPipelineOptions): Promise<RunResult>;
|