@smthrs/plugin 1.0.0-rc.7 → 1.0.0-rc.9
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 +3 -1
- package/dist/cjs/Plugins.d.ts.map +1 -1
- package/dist/cjs/Plugins.js +17 -8
- package/dist/cjs/Plugins.js.map +2 -2
- package/dist/cjs/Resolve.d.ts.map +1 -1
- package/dist/cjs/Resolve.js +22 -19
- package/dist/cjs/Resolve.js.map +2 -2
- package/dist/cjs/internal/snapshotCacheEnvironment.d.ts +6 -2
- package/dist/cjs/internal/snapshotCacheEnvironment.d.ts.map +1 -1
- package/dist/cjs/internal/snapshotCacheEnvironment.js +2 -2
- package/dist/cjs/internal/snapshotCacheEnvironment.js.map +2 -2
- package/dist/esm/Plugins.d.ts.map +1 -1
- package/dist/esm/Plugins.js +21 -9
- package/dist/esm/Plugins.js.map +1 -1
- package/dist/esm/Resolve.d.ts.map +1 -1
- package/dist/esm/Resolve.js +23 -21
- package/dist/esm/Resolve.js.map +1 -1
- package/dist/esm/internal/snapshotCacheEnvironment.d.ts +6 -2
- package/dist/esm/internal/snapshotCacheEnvironment.d.ts.map +1 -1
- package/dist/esm/internal/snapshotCacheEnvironment.js +4 -3
- package/dist/esm/internal/snapshotCacheEnvironment.js.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +6 -4
- package/docs/guides/cache-identity.md +1 -1
- package/docs/installation.md +5 -1
- package/docs/quickstart.md +2 -1
- package/docs/troubleshooting.md +2 -1
- package/package.json +4 -4
- package/src/Plugins.ts +25 -8
- package/src/Resolve.ts +26 -22
- package/src/internal/snapshotCacheEnvironment.ts +5 -4
package/docs/installation.md
CHANGED
|
@@ -7,7 +7,11 @@ sidebar:
|
|
|
7
7
|
|
|
8
8
|
## Install the package
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install @smthrs/plugin@next effect
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The `next` tag carries the 1.0 release candidates.
|
|
11
15
|
|
|
12
16
|
Install `effect` explicitly, in the range `^4.0.4`. The kernel depends on it, but your
|
|
13
17
|
plugins import `Effect`, `Layer`, and `Option` directly, so `effect` belongs in
|
package/docs/quickstart.md
CHANGED
|
@@ -16,7 +16,8 @@ defines the plugins and runs the host. There are no services to provide.
|
|
|
16
16
|
## Prerequisites
|
|
17
17
|
|
|
18
18
|
- Node.js 26.4.0 or later.
|
|
19
|
-
- A TypeScript project with the package installed
|
|
19
|
+
- A TypeScript project with the package installed:
|
|
20
|
+
`npm install @smthrs/plugin@next effect`.
|
|
20
21
|
|
|
21
22
|
## Declare the host's hook catalog
|
|
22
23
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -45,7 +45,8 @@ At startup, `Options.hooks` labelled `config` with a kind other than
|
|
|
45
45
|
`waterfall`, or `configResolved` with a kind other than `parallel`; the path
|
|
46
46
|
names the entry, such as `$options.hooks.config`. At dispatch, a method was
|
|
47
47
|
called for a hook the catalog declares with a different kind, which only a cast
|
|
48
|
-
around the typed dispatcher can reach
|
|
48
|
+
around the typed dispatcher can reach, or for a hook a hand-built `Resolved`
|
|
49
|
+
gave handlers without declaring it in `kinds`. In every case no handler ran.
|
|
49
50
|
|
|
50
51
|
**What to change.** Make the catalog agree with the hook interface: spread
|
|
51
52
|
`engineHooks` for the shared hooks and write each of your own hooks with the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/plugin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.9",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -145,8 +145,8 @@
|
|
|
145
145
|
"tag": "next"
|
|
146
146
|
},
|
|
147
147
|
"dependencies": {
|
|
148
|
-
"@smthrs/canonical": "1.0.0-rc.
|
|
149
|
-
"@smthrs/flow": "1.0.0-rc.
|
|
148
|
+
"@smthrs/canonical": "1.0.0-rc.9",
|
|
149
|
+
"@smthrs/flow": "1.0.0-rc.9"
|
|
150
150
|
},
|
|
151
151
|
"devDependencies": {
|
|
152
152
|
"@effect/language-service": "0.87.2",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"effect": "^4.0.4"
|
|
172
172
|
},
|
|
173
173
|
"scripts": {
|
|
174
|
-
"lint": "eslint src --max-warnings=0 && dprint check",
|
|
174
|
+
"lint": "eslint src examples --max-warnings=0 && dprint check",
|
|
175
175
|
"format": "dprint fmt",
|
|
176
176
|
"build": "node scripts/build.mjs",
|
|
177
177
|
"check": "tsc -b tsconfig.json && tsc -p tsconfig.test.json --noEmit",
|
package/src/Plugins.ts
CHANGED
|
@@ -32,8 +32,23 @@ const empty: ReadonlyArray<HandlerRecord> = Object.freeze([])
|
|
|
32
32
|
// The typed catalog and the runtime catalog can only drift when a host casts
|
|
33
33
|
// around `KeysOfKind`; refuse before any handler runs rather than execute a
|
|
34
34
|
// hook under semantics its declaration never promised.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// A hook the catalog does not declare dispatches as empty, unless a
|
|
36
|
+
// hand-built resolution registered handlers for it: those have no declared
|
|
37
|
+
// kind to honor, so they are refused too.
|
|
38
|
+
const kindMismatch = (
|
|
39
|
+
resolved: Resolved<unknown>,
|
|
40
|
+
hook: string,
|
|
41
|
+
kind: HookKind,
|
|
42
|
+
handlers: ReadonlyArray<HandlerRecord>
|
|
43
|
+
): PluginError | undefined => {
|
|
44
|
+
if (!Object.hasOwn(resolved.kinds, hook)) {
|
|
45
|
+
if (handlers.length === 0) return undefined
|
|
46
|
+
return new PluginError({
|
|
47
|
+
code: "hook_kind_mismatch",
|
|
48
|
+
message: `hook "${hook}" has handlers but is not declared in the runtime catalog`,
|
|
49
|
+
hook
|
|
50
|
+
})
|
|
51
|
+
}
|
|
37
52
|
const declared = resolved.kinds[hook]
|
|
38
53
|
if (declared === kind) return undefined
|
|
39
54
|
return new PluginError({
|
|
@@ -125,7 +140,7 @@ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
|
|
|
125
140
|
|
|
126
141
|
const sequential = ((hook: string, ...args: Array<unknown>) =>
|
|
127
142
|
Effect.gen(function*() {
|
|
128
|
-
const mismatch = kindMismatch(resolved, hook, "sequential")
|
|
143
|
+
const mismatch = kindMismatch(resolved, hook, "sequential", handlers(hook))
|
|
129
144
|
if (mismatch !== undefined) return yield* Effect.fail(mismatch)
|
|
130
145
|
const results: Array<unknown> = []
|
|
131
146
|
for (const record of handlers(hook)) {
|
|
@@ -135,7 +150,7 @@ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
|
|
|
135
150
|
})) as Service<H>["sequential"]
|
|
136
151
|
|
|
137
152
|
const parallel = ((hook: string, ...args: Array<unknown>) => {
|
|
138
|
-
const mismatch = kindMismatch(resolved, hook, "parallel")
|
|
153
|
+
const mismatch = kindMismatch(resolved, hook, "parallel", handlers(hook))
|
|
139
154
|
if (mismatch !== undefined) return Effect.succeed([mismatch])
|
|
140
155
|
return Effect.forEach(
|
|
141
156
|
handlers(hook),
|
|
@@ -149,7 +164,7 @@ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
|
|
|
149
164
|
|
|
150
165
|
const first = ((hook: string, ...args: Array<unknown>) =>
|
|
151
166
|
Effect.gen(function*() {
|
|
152
|
-
const mismatch = kindMismatch(resolved, hook, "first")
|
|
167
|
+
const mismatch = kindMismatch(resolved, hook, "first", handlers(hook))
|
|
153
168
|
if (mismatch !== undefined) return yield* Effect.fail(mismatch)
|
|
154
169
|
for (const record of handlers(hook)) {
|
|
155
170
|
const result = yield* runHandler(record, args)
|
|
@@ -171,7 +186,7 @@ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
|
|
|
171
186
|
const waterfall =
|
|
172
187
|
((hook: string, initial: unknown, merge: (previous: unknown, patch: unknown) => unknown) =>
|
|
173
188
|
Effect.gen(function*() {
|
|
174
|
-
const mismatch = kindMismatch(resolved, hook, "waterfall")
|
|
189
|
+
const mismatch = kindMismatch(resolved, hook, "waterfall", handlers(hook))
|
|
175
190
|
if (mismatch !== undefined) return yield* Effect.fail(mismatch)
|
|
176
191
|
let value = initial
|
|
177
192
|
for (const record of handlers(hook)) {
|
|
@@ -186,13 +201,15 @@ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
|
|
|
186
201
|
message: cause.message,
|
|
187
202
|
plugin: record.plugin,
|
|
188
203
|
hook: record.hook,
|
|
189
|
-
path: cause.path
|
|
204
|
+
path: cause.path,
|
|
205
|
+
cause
|
|
190
206
|
})
|
|
191
207
|
: new PluginError({
|
|
192
208
|
code: "config_invalid",
|
|
193
209
|
message: `waterfall hook "${record.hook}" in plugin "${record.plugin}" returned an invalid patch`,
|
|
194
210
|
plugin: record.plugin,
|
|
195
|
-
hook: record.hook
|
|
211
|
+
hook: record.hook,
|
|
212
|
+
cause
|
|
196
213
|
})
|
|
197
214
|
})
|
|
198
215
|
}
|
package/src/Resolve.ts
CHANGED
|
@@ -161,22 +161,25 @@ const childPath = (path: string, key: string): string => {
|
|
|
161
161
|
const ownData = (
|
|
162
162
|
input: unknown,
|
|
163
163
|
path: string,
|
|
164
|
-
allowed?: ReadonlySet<string
|
|
164
|
+
allowed?: ReadonlySet<string>,
|
|
165
|
+
fields: { readonly plugin?: string; readonly hook?: string } = {}
|
|
165
166
|
): Readonly<Record<string, unknown>> => {
|
|
166
167
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
167
|
-
throw invalidPlugin(path, "must be an ordinary record")
|
|
168
|
+
throw invalidPlugin(path, "must be an ordinary record", fields)
|
|
168
169
|
}
|
|
169
170
|
const prototype = Object.getPrototypeOf(input)
|
|
170
|
-
if (prototype !== Object.prototype && prototype !== null)
|
|
171
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
172
|
+
throw invalidPlugin(path, "must be an ordinary record", fields)
|
|
173
|
+
}
|
|
171
174
|
const output: Record<string, unknown> = {}
|
|
172
175
|
for (const key of Reflect.ownKeys(input)) {
|
|
173
|
-
if (typeof key !== "string") throw invalidPlugin(path, "must not contain symbol keys")
|
|
176
|
+
if (typeof key !== "string") throw invalidPlugin(path, "must not contain symbol keys", fields)
|
|
174
177
|
if (allowed !== undefined && !allowed.has(key)) {
|
|
175
|
-
throw invalidPlugin(childPath(path, key), "contains an unknown property")
|
|
178
|
+
throw invalidPlugin(childPath(path, key), "contains an unknown property", fields)
|
|
176
179
|
}
|
|
177
180
|
const descriptor = Object.getOwnPropertyDescriptor(input, key)
|
|
178
181
|
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
179
|
-
throw invalidPlugin(childPath(path, key), "must be an enumerable data property")
|
|
182
|
+
throw invalidPlugin(childPath(path, key), "must be an enumerable data property", fields)
|
|
180
183
|
}
|
|
181
184
|
Object.defineProperty(output, key, { value: descriptor.value, enumerable: true })
|
|
182
185
|
}
|
|
@@ -249,7 +252,7 @@ const snapshotHook = (
|
|
|
249
252
|
if (entry === undefined) {
|
|
250
253
|
throw invalidPlugin(path, "must be a function or a hook object", { plugin, hook })
|
|
251
254
|
}
|
|
252
|
-
const values = ownData(entry, path, new Set(["order", "handler"]))
|
|
255
|
+
const values = ownData(entry, path, new Set(["order", "handler"]), { plugin, hook })
|
|
253
256
|
if (typeof values.handler !== "function") {
|
|
254
257
|
throw invalidPlugin(`${path}.handler`, "must be a function", { plugin, hook })
|
|
255
258
|
}
|
|
@@ -285,11 +288,11 @@ const snapshotPlugin = <H>(
|
|
|
285
288
|
|
|
286
289
|
let hooks: Readonly<Record<string, unknown>> | undefined
|
|
287
290
|
if (values.hooks !== undefined) {
|
|
288
|
-
const rawHooks = ownData(values.hooks, `${path}.hooks
|
|
291
|
+
const rawHooks = ownData(values.hooks, `${path}.hooks`, undefined, { plugin: name })
|
|
289
292
|
const snapshot: Record<string, unknown> = Object.create(null)
|
|
290
293
|
for (const [index, hook] of Object.keys(rawHooks).entries()) {
|
|
291
294
|
if (!validName(hook)) {
|
|
292
|
-
throw invalidPlugin(`${path}.hooks[${index}]`, "contains an invalid hook name")
|
|
295
|
+
throw invalidPlugin(`${path}.hooks[${index}]`, "contains an invalid hook name", { plugin: name })
|
|
293
296
|
}
|
|
294
297
|
const entry = rawHooks[hook]
|
|
295
298
|
Object.defineProperty(snapshot, hook, {
|
|
@@ -360,7 +363,7 @@ const flatten = <H>(input: unknown): ReadonlyArray<AdmittedPlugin<H>> => {
|
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
const included = <H>(
|
|
363
|
-
plugin:
|
|
366
|
+
{ path, plugin }: AdmittedPlugin<H>,
|
|
364
367
|
config: FlowsConfig,
|
|
365
368
|
target: "engine" | "harness"
|
|
366
369
|
): Effect.Effect<boolean, PluginError> => {
|
|
@@ -370,14 +373,14 @@ const included = <H>(
|
|
|
370
373
|
return Effect.try({
|
|
371
374
|
try: () => apply(config),
|
|
372
375
|
catch: () =>
|
|
373
|
-
failure("apply_failed", `plugin "${plugin.name}" apply predicate failed`,
|
|
376
|
+
failure("apply_failed", `plugin "${plugin.name}" apply predicate failed`, `${path}.apply`, {
|
|
374
377
|
plugin: plugin.name
|
|
375
378
|
})
|
|
376
379
|
}).pipe(
|
|
377
380
|
Effect.flatMap((result) =>
|
|
378
381
|
typeof result === "boolean"
|
|
379
382
|
? Effect.succeed(result)
|
|
380
|
-
: Effect.fail(invalidPlugin(
|
|
383
|
+
: Effect.fail(invalidPlugin(`${path}.apply`, "predicate must return a boolean", { plugin: plugin.name }))
|
|
381
384
|
)
|
|
382
385
|
)
|
|
383
386
|
}
|
|
@@ -443,7 +446,7 @@ export const resolve = <H = FlowsHooks>(
|
|
|
443
446
|
: invalidPlugin("$", "could not be inspected without executing user code")
|
|
444
447
|
})
|
|
445
448
|
const target = raw.target ?? "engine"
|
|
446
|
-
const selected = yield* Effect.filter(admitted, (
|
|
449
|
+
const selected = yield* Effect.filter(admitted, (entry) => included(entry, config, target))
|
|
447
450
|
|
|
448
451
|
let handlerCount = 0
|
|
449
452
|
for (const { path, plugin } of selected) {
|
|
@@ -470,24 +473,25 @@ export const resolve = <H = FlowsHooks>(
|
|
|
470
473
|
}
|
|
471
474
|
|
|
472
475
|
const seen = new Set<string>()
|
|
473
|
-
for (const { plugin } of selected) {
|
|
476
|
+
for (const { path, plugin } of selected) {
|
|
474
477
|
if (seen.has(plugin.name)) {
|
|
475
478
|
return yield* Effect.fail(failure(
|
|
476
479
|
"duplicate_name",
|
|
477
480
|
`duplicate plugin name "${plugin.name}"`,
|
|
478
|
-
|
|
481
|
+
`${path}.name`,
|
|
479
482
|
{ plugin: plugin.name }
|
|
480
483
|
))
|
|
481
484
|
}
|
|
482
485
|
seen.add(plugin.name)
|
|
483
486
|
}
|
|
484
487
|
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
488
|
+
const orderedEntries = selected
|
|
489
|
+
.map((entry, index) => ({ entry, index }))
|
|
490
|
+
.sort((left, right) =>
|
|
491
|
+
rank(left.entry.plugin.enforce) - rank(right.entry.plugin.enforce) || left.index - right.index
|
|
492
|
+
)
|
|
493
|
+
.map(({ entry }) => entry)
|
|
494
|
+
const plugins = Object.freeze(orderedEntries.map(({ plugin }) => plugin))
|
|
491
495
|
const entries: Array<readonly [string, ReadonlyArray<HandlerRecord>]> = []
|
|
492
496
|
for (const hook of Object.keys(known)) {
|
|
493
497
|
const records: Array<{
|
|
@@ -514,7 +518,7 @@ export const resolve = <H = FlowsHooks>(
|
|
|
514
518
|
.map(({ record }) => record)
|
|
515
519
|
if (ordered.length > 0) entries.push([hook, Object.freeze(ordered)])
|
|
516
520
|
}
|
|
517
|
-
const cacheEnvironment = yield* snapshotCacheEnvironment(raw.cacheEnvironment,
|
|
521
|
+
const cacheEnvironment = yield* snapshotCacheEnvironment(raw.cacheEnvironment, orderedEntries)
|
|
518
522
|
return Object.freeze({
|
|
519
523
|
plugins,
|
|
520
524
|
handlers: ImmutableMap.make(entries),
|
|
@@ -18,24 +18,25 @@ const escapePluginIdentityPart = (value: string): string =>
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Admits a caller cache environment and prepends one escaped `name@version`
|
|
21
|
-
* identity per plugin, in resolved order. An absent input stays absent.
|
|
21
|
+
* identity per plugin, in resolved order. An absent input stays absent. A
|
|
22
|
+
* versionless plugin is refused at the dollar-path it was admitted from.
|
|
22
23
|
*
|
|
23
24
|
* @private
|
|
24
25
|
* @since 1.0.0-rc.0
|
|
25
26
|
*/
|
|
26
27
|
export const snapshotCacheEnvironment = <H>(
|
|
27
28
|
input: unknown,
|
|
28
|
-
plugins: ReadonlyArray<FlowsPlugin<H
|
|
29
|
+
plugins: ReadonlyArray<{ readonly plugin: FlowsPlugin<H>; readonly path: string }>
|
|
29
30
|
): Effect.Effect<Action.CacheEnvironment | undefined, PluginError> => {
|
|
30
31
|
if (input === undefined) return Effect.succeed(undefined)
|
|
31
32
|
const pluginIdentities: Array<string> = []
|
|
32
|
-
for (const plugin of plugins) {
|
|
33
|
+
for (const { path, plugin } of plugins) {
|
|
33
34
|
if (plugin.version === undefined) {
|
|
34
35
|
return Effect.fail(
|
|
35
36
|
new PluginError({
|
|
36
37
|
code: "cache_environment_invalid",
|
|
37
38
|
message: `cache environment requires a version for plugin "${plugin.name}"`,
|
|
38
|
-
path:
|
|
39
|
+
path: `${path}.version`,
|
|
39
40
|
plugin: plugin.name
|
|
40
41
|
})
|
|
41
42
|
)
|