@meith/plugin-kit 0.21.1 → 0.22.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/package.json +3 -3
- package/src/host.ts +35 -13
- package/src/index.ts +4 -0
- package/src/navigation.ts +0 -1
- package/src/plugin.ts +29 -22
- package/src/settings.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/plugin-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "The SDK for writing a Meith plugin: typed manifests, hooks, routes, pages and migrations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@meith/core": "^0.
|
|
24
|
-
"@meith/theme-kit": "^0.
|
|
23
|
+
"@meith/core": "^0.22.0",
|
|
24
|
+
"@meith/theme-kit": "^0.22.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": "^19.2.0"
|
package/src/host.ts
CHANGED
|
@@ -2,7 +2,13 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { HOOKS, type HookName } from './hooks'
|
|
4
4
|
import type { HookContext, HookValue } from './payloads'
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
HookRegistration,
|
|
7
|
+
HookRuntime,
|
|
8
|
+
PluginContribution,
|
|
9
|
+
PluginDefinition,
|
|
10
|
+
PluginRuntimeContext,
|
|
11
|
+
} from './plugin'
|
|
6
12
|
import type { PluginRegion, PluginRegionContext } from './regions'
|
|
7
13
|
|
|
8
14
|
export interface HostLogger {
|
|
@@ -17,10 +23,6 @@ export interface PluginFailure {
|
|
|
17
23
|
readonly threshold: number
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
* Where a failure goes once the host has counted it. The host stays
|
|
22
|
-
* synchronous and fire-and-forgets; whoever supplies this owns the write.
|
|
23
|
-
*/
|
|
24
26
|
export interface PluginHealthSink {
|
|
25
27
|
readonly failed: (failure: PluginFailure) => void
|
|
26
28
|
}
|
|
@@ -30,6 +32,8 @@ export interface DurablyDisabledPlugin {
|
|
|
30
32
|
readonly reason: string
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export type PluginRuntimeProvider = (pluginKey: string) => Promise<PluginRuntimeContext>
|
|
36
|
+
|
|
33
37
|
export interface PluginHostOptions {
|
|
34
38
|
readonly plugins: readonly PluginDefinition[]
|
|
35
39
|
readonly logger?: HostLogger | undefined
|
|
@@ -37,6 +41,7 @@ export interface PluginHostOptions {
|
|
|
37
41
|
readonly slowCallMs?: number | undefined
|
|
38
42
|
readonly now?: (() => number) | undefined
|
|
39
43
|
readonly health?: PluginHealthSink | undefined
|
|
44
|
+
readonly runtime?: PluginRuntimeProvider | undefined
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export interface PluginHealth {
|
|
@@ -52,7 +57,7 @@ export interface PluginHealth {
|
|
|
52
57
|
readonly lastError: { readonly hook: string; readonly message: string } | null
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
type StoredHandler = (value: unknown, context: unknown) => unknown
|
|
60
|
+
type StoredHandler = (value: unknown, context: unknown, runtime: HookRuntime) => unknown
|
|
56
61
|
|
|
57
62
|
interface Entry {
|
|
58
63
|
readonly pluginKey: string
|
|
@@ -86,6 +91,7 @@ export class PluginHost {
|
|
|
86
91
|
readonly #slowCallMs: number
|
|
87
92
|
readonly #now: () => number
|
|
88
93
|
readonly #health: PluginHealthSink
|
|
94
|
+
readonly #runtime: PluginRuntimeProvider
|
|
89
95
|
|
|
90
96
|
constructor(options: PluginHostOptions) {
|
|
91
97
|
this.#logger = options.logger ?? { warn: () => {}, error: () => {} }
|
|
@@ -93,6 +99,15 @@ export class PluginHost {
|
|
|
93
99
|
this.#slowCallMs = options.slowCallMs ?? 50
|
|
94
100
|
this.#now = options.now ?? (() => performance.now())
|
|
95
101
|
this.#health = options.health ?? { failed: () => {} }
|
|
102
|
+
this.#runtime =
|
|
103
|
+
options.runtime ??
|
|
104
|
+
((pluginKey) =>
|
|
105
|
+
Promise.reject(
|
|
106
|
+
new Error(
|
|
107
|
+
`plugin "${pluginKey}": this host was built without a runtime provider, so a hook ` +
|
|
108
|
+
'handler cannot reach settings, data, grants, users or notifications here.',
|
|
109
|
+
),
|
|
110
|
+
))
|
|
96
111
|
|
|
97
112
|
for (const plugin of options.plugins) {
|
|
98
113
|
this.#stats.set(plugin.key, {
|
|
@@ -158,7 +173,9 @@ export class PluginHost {
|
|
|
158
173
|
for (const entry of entries) {
|
|
159
174
|
if (!this.#isEnabled(entry.pluginKey)) continue
|
|
160
175
|
|
|
161
|
-
const result = await this.#call(entry.pluginKey, name, () =>
|
|
176
|
+
const result = await this.#call(entry.pluginKey, name, () =>
|
|
177
|
+
entry.handler(current, context, this.#runtimeFor(entry.pluginKey)),
|
|
178
|
+
)
|
|
162
179
|
|
|
163
180
|
if (result.ok && result.value !== undefined) current = result.value as HookValue<K>
|
|
164
181
|
}
|
|
@@ -175,7 +192,17 @@ export class PluginHost {
|
|
|
175
192
|
|
|
176
193
|
for (const entry of entries) {
|
|
177
194
|
if (!this.#isEnabled(entry.pluginKey)) continue
|
|
178
|
-
await this.#call(entry.pluginKey, name, () =>
|
|
195
|
+
await this.#call(entry.pluginKey, name, () =>
|
|
196
|
+
entry.handler(value, context, this.#runtimeFor(entry.pluginKey)),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#runtimeFor(pluginKey: string): HookRuntime {
|
|
202
|
+
let pending: Promise<PluginRuntimeContext> | null = null
|
|
203
|
+
return () => {
|
|
204
|
+
pending ??= this.#runtime(pluginKey)
|
|
205
|
+
return pending
|
|
179
206
|
}
|
|
180
207
|
}
|
|
181
208
|
|
|
@@ -234,11 +261,6 @@ export class PluginHost {
|
|
|
234
261
|
}
|
|
235
262
|
}
|
|
236
263
|
|
|
237
|
-
/**
|
|
238
|
-
* Reconcile against the durable record. The stored rows are the answer,
|
|
239
|
-
* not a hint: a plugin an operator has cleared comes back without a
|
|
240
|
-
* restart, and one another instance switched off is off here too.
|
|
241
|
-
*/
|
|
242
264
|
setDurablyDisabled(rows: readonly DurablyDisabledPlugin[]): void {
|
|
243
265
|
const disabled = new Map(rows.map((row) => [row.key, row.reason]))
|
|
244
266
|
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
type PluginHealthSink,
|
|
18
18
|
PluginHost,
|
|
19
19
|
type PluginHostOptions,
|
|
20
|
+
type PluginRuntimeProvider,
|
|
20
21
|
} from './host'
|
|
21
22
|
export {
|
|
22
23
|
type PluginNavigationPlacement,
|
|
@@ -44,6 +45,7 @@ export {
|
|
|
44
45
|
type FilterHandler,
|
|
45
46
|
type HookHandler,
|
|
46
47
|
type HookRegistration,
|
|
48
|
+
type HookRuntime,
|
|
47
49
|
MAX_ROUTE_BODY_BYTES,
|
|
48
50
|
type PluginAdminPage,
|
|
49
51
|
type PluginAdminPageContext,
|
|
@@ -76,6 +78,8 @@ export {
|
|
|
76
78
|
pluginSettingKey,
|
|
77
79
|
pluginTablePrefix,
|
|
78
80
|
pluginTaskId,
|
|
81
|
+
unavailableHookRuntime,
|
|
82
|
+
unavailablePluginRuntime,
|
|
79
83
|
} from './plugin'
|
|
80
84
|
export {
|
|
81
85
|
createRouteRateLimiter,
|
package/src/navigation.ts
CHANGED
|
@@ -9,7 +9,6 @@ export interface PluginNavigationPlacement {
|
|
|
9
9
|
readonly key: string
|
|
10
10
|
readonly href: string
|
|
11
11
|
readonly audience: PluginNavigationAudience
|
|
12
|
-
/** The namespaced key of the sibling item this one sits under by default. */
|
|
13
12
|
readonly parentKey: string | null
|
|
14
13
|
readonly label: string
|
|
15
14
|
readonly labelKey: string | null
|
package/src/plugin.ts
CHANGED
|
@@ -5,16 +5,29 @@ import type { Translator } from '@meith/theme-kit'
|
|
|
5
5
|
import { type HOOKS, type HookName, isHookName } from './hooks'
|
|
6
6
|
import type { HookContext, HookValue } from './payloads'
|
|
7
7
|
import { isPluginRegion, type PluginRegion, type PluginRegionContext } from './regions'
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
type PluginData,
|
|
10
|
+
type PluginGrants,
|
|
11
|
+
type PluginNotify,
|
|
12
|
+
type PluginUsers,
|
|
13
|
+
unavailablePluginData,
|
|
14
|
+
unavailablePluginGrants,
|
|
15
|
+
unavailablePluginNotify,
|
|
16
|
+
unavailablePluginUsers,
|
|
17
|
+
} from './runtime'
|
|
18
|
+
|
|
19
|
+
export type HookRuntime = () => Promise<PluginRuntimeContext>
|
|
9
20
|
|
|
10
21
|
export type FilterHandler<K extends HookName> = (
|
|
11
22
|
value: HookValue<K>,
|
|
12
23
|
context: HookContext<K>,
|
|
24
|
+
runtime: HookRuntime,
|
|
13
25
|
) => HookValue<K> | Promise<HookValue<K>>
|
|
14
26
|
|
|
15
27
|
export type EventHandler<K extends HookName> = (
|
|
16
28
|
value: HookValue<K>,
|
|
17
29
|
context: HookContext<K>,
|
|
30
|
+
runtime: HookRuntime,
|
|
18
31
|
) => void | Promise<void>
|
|
19
32
|
|
|
20
33
|
export type HookHandler<K extends HookName> = (typeof HOOKS)[K]['kind'] extends 'filter'
|
|
@@ -153,28 +166,13 @@ type TranslationArgs = Parameters<Translator['t']>[1]
|
|
|
153
166
|
|
|
154
167
|
export type PluginNavigationAudience = 'all' | 'guests' | 'members' | 'staff'
|
|
155
168
|
|
|
156
|
-
/**
|
|
157
|
-
* A board navigation entry a plugin asks for.
|
|
158
|
-
*
|
|
159
|
-
* It is a **request, not a placement**: the host writes it into the board's own
|
|
160
|
-
* navigation table, where an operator renames, reorders, nests, scopes or hides
|
|
161
|
-
* it like any other item. A plugin that appended to the header model instead
|
|
162
|
-
* would put a link where no operator could reach it.
|
|
163
|
-
*/
|
|
164
169
|
export interface PluginNavigationItem {
|
|
165
170
|
readonly key: string
|
|
166
171
|
readonly label: string
|
|
167
172
|
readonly labelKey?: string | undefined
|
|
168
173
|
readonly labelArgs?: TranslationArgs | undefined
|
|
169
|
-
/** A page path of this plugin's own — '' is its index page. */
|
|
170
174
|
readonly path: string
|
|
171
175
|
readonly audience?: PluginNavigationAudience | undefined
|
|
172
|
-
/**
|
|
173
|
-
* The `key` of another of this plugin's navigation items to sit under by
|
|
174
|
-
* default. The board's navigation is one level deep, so the item named here
|
|
175
|
-
* must itself be top-level. Like `audience`, it only seeds the row — the
|
|
176
|
-
* operator re-nests it like any other item.
|
|
177
|
-
*/
|
|
178
176
|
readonly under?: string | undefined
|
|
179
177
|
}
|
|
180
178
|
|
|
@@ -191,6 +189,21 @@ export interface PluginRuntimeContext {
|
|
|
191
189
|
readonly notify: PluginNotify
|
|
192
190
|
}
|
|
193
191
|
|
|
192
|
+
export function unavailablePluginRuntime(reason: string): PluginRuntimeContext {
|
|
193
|
+
return {
|
|
194
|
+
settings: {},
|
|
195
|
+
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
|
196
|
+
grants: unavailablePluginGrants(reason),
|
|
197
|
+
data: unavailablePluginData(reason),
|
|
198
|
+
users: unavailablePluginUsers(reason),
|
|
199
|
+
notify: unavailablePluginNotify(reason),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function unavailableHookRuntime(reason: string): HookRuntime {
|
|
204
|
+
return () => Promise.resolve(unavailablePluginRuntime(reason))
|
|
205
|
+
}
|
|
206
|
+
|
|
194
207
|
export interface PluginNotificationKind {
|
|
195
208
|
readonly key: string
|
|
196
209
|
readonly title: string
|
|
@@ -231,12 +244,6 @@ export interface PluginDefinition {
|
|
|
231
244
|
readonly onUninstall?: ((context: PluginRuntimeContext) => Promise<void> | void) | undefined
|
|
232
245
|
}
|
|
233
246
|
|
|
234
|
-
/**
|
|
235
|
-
* The rule a plugin key (and, by marketplace-gen.mjs's own mirrored copy, a
|
|
236
|
-
* marketplace listing key) has to satisfy. Exported so
|
|
237
|
-
* scripts/marketplace-gen.test.ts can pin its own copy directly against
|
|
238
|
-
* this one rather than trusting the two stay in sync by comment alone.
|
|
239
|
-
*/
|
|
240
247
|
export const KEY_PATTERN = /^[a-z][a-z0-9-]{1,39}$/
|
|
241
248
|
const SETTING_KEY_PATTERN = /^[a-z][a-z0-9_]{1,39}$/
|
|
242
249
|
const MIGRATION_ID_PATTERN = /^\d{4}_[a-z0-9_]{1,60}$/
|
package/src/settings.ts
CHANGED
|
@@ -47,11 +47,6 @@ export function parsePluginSetting(setting: PluginSetting, raw: string): PluginS
|
|
|
47
47
|
return raw
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* Matches a candidate select value against a setting's declared options —
|
|
52
|
-
* trimmed and case-insensitively — and returns the *option's own* casing.
|
|
53
|
-
* See "Settings" in docs/plugin-api.md.
|
|
54
|
-
*/
|
|
55
50
|
function matchSelectOption(
|
|
56
51
|
options: readonly { readonly value: string }[],
|
|
57
52
|
candidate: PluginSettingValue | null,
|