@rohal12/spindle 0.52.4 → 0.52.6

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.
Files changed (47) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/dist/pkg/headless.js +1502 -1521
  3. package/dist/pkg/macro-registry.json +255 -0
  4. package/package.json +1 -1
  5. package/src/class-registry.ts +6 -182
  6. package/src/components/macros/Button.tsx +2 -4
  7. package/src/components/macros/Checkbox.tsx +6 -13
  8. package/src/components/macros/Computed.tsx +38 -55
  9. package/src/components/macros/Cycle.tsx +1 -0
  10. package/src/components/macros/Dialog.tsx +7 -8
  11. package/src/components/macros/Do.tsx +2 -1
  12. package/src/components/macros/For.tsx +27 -78
  13. package/src/components/macros/Goto.tsx +6 -9
  14. package/src/components/macros/Include.tsx +10 -57
  15. package/src/components/macros/Listbox.tsx +1 -0
  16. package/src/components/macros/MacroError.tsx +37 -0
  17. package/src/components/macros/MacroLink.tsx +9 -24
  18. package/src/components/macros/Meter.tsx +19 -37
  19. package/src/components/macros/Numberbox.tsx +5 -32
  20. package/src/components/macros/PassageDisplay.tsx +37 -47
  21. package/src/components/macros/Print.tsx +1 -0
  22. package/src/components/macros/Radiobutton.tsx +8 -38
  23. package/src/components/macros/Repeat.tsx +3 -3
  24. package/src/components/macros/Set.tsx +3 -29
  25. package/src/components/macros/Switch.tsx +36 -37
  26. package/src/components/macros/Textarea.tsx +2 -28
  27. package/src/components/macros/Textbox.tsx +2 -29
  28. package/src/components/macros/Type.tsx +3 -3
  29. package/src/components/macros/Unset.tsx +17 -40
  30. package/src/components/macros/Watch.tsx +23 -91
  31. package/src/components/macros/WidgetInvocation.tsx +9 -71
  32. package/src/components/macros/detached-body.tsx +2 -2
  33. package/src/components/macros/input-macro.ts +48 -0
  34. package/src/components/macros/locals-scope.tsx +60 -0
  35. package/src/components/macros/macro-args.ts +280 -0
  36. package/src/components/macros/option-utils.ts +7 -12
  37. package/src/define-macro.ts +47 -30
  38. package/src/execute-mutation.ts +17 -97
  39. package/src/hooks/use-render-options.ts +26 -0
  40. package/src/markup/render.tsx +2 -4
  41. package/src/registry.ts +48 -0
  42. package/src/saves/save-manager.ts +190 -259
  43. package/src/saves/storage.ts +229 -402
  44. package/src/store.ts +244 -310
  45. package/src/structural.ts +388 -0
  46. package/types/index.d.ts +69 -6
  47. package/types/tooling.d.ts +33 -1
@@ -0,0 +1,388 @@
1
+ // Structural operations on story values: copying, comparing, and finding
2
+ // and merging the differences between two versions of a value. Mutation
3
+ // commits, history and save hooks all go through these, so they agree on
4
+ // what a change is.
5
+
6
+ import { registeredClassName } from './class-registry';
7
+ import { hasOwn } from './utils/namespace';
8
+ import { deleteByPath, getByPath, setByPath } from './utils/object-path';
9
+
10
+ /**
11
+ * Set an own enumerable property. A "__proto__" key is defined as an own
12
+ * property (as JSON.parse does) instead of replacing the prototype.
13
+ */
14
+ export function setOwn(target: object, key: string, value: unknown): void {
15
+ if (key === '__proto__') {
16
+ Object.defineProperty(target, key, {
17
+ value,
18
+ enumerable: true,
19
+ writable: true,
20
+ configurable: true,
21
+ });
22
+ } else {
23
+ (target as Record<string, unknown>)[key] = value;
24
+ }
25
+ }
26
+
27
+ // --- Deep Clone ---
28
+
29
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
30
+ if (typeof value !== 'object' || value === null) return false;
31
+ const proto = Object.getPrototypeOf(value);
32
+ return proto === Object.prototype || proto === null;
33
+ }
34
+
35
+ export interface DeepCloneOptions {
36
+ /**
37
+ * Return instances of unregistered classes (DOM nodes, promises, other
38
+ * library objects) by reference instead of copying their own keys into a
39
+ * plain object, which would lose their prototype and identity.
40
+ */
41
+ keepUnregistered?: boolean;
42
+ }
43
+
44
+ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
45
+ const seen = new Map<object, object>();
46
+
47
+ function clone(val: unknown): unknown {
48
+ if (val === null || typeof val !== 'object') return val;
49
+
50
+ const obj = val as object;
51
+ if (seen.has(obj)) return seen.get(obj);
52
+
53
+ if (val instanceof Date) return new Date(val.getTime()) as unknown;
54
+ if (val instanceof RegExp)
55
+ return new RegExp(val.source, val.flags) as unknown;
56
+
57
+ if (Array.isArray(val)) {
58
+ const arr: unknown[] = [];
59
+ seen.set(obj, arr);
60
+ for (let i = 0; i < val.length; i++) {
61
+ arr[i] = clone(val[i]);
62
+ }
63
+ return arr;
64
+ }
65
+
66
+ if (val instanceof Map) {
67
+ const copy = new Map();
68
+ seen.set(obj, copy);
69
+ for (const [k, v] of val) {
70
+ copy.set(clone(k), clone(v));
71
+ }
72
+ return copy;
73
+ }
74
+
75
+ if (val instanceof Set) {
76
+ const copy = new Set();
77
+ seen.set(obj, copy);
78
+ for (const v of val) {
79
+ copy.add(clone(v));
80
+ }
81
+ return copy;
82
+ }
83
+
84
+ // A registered class instance keeps its class, and a plain object its
85
+ // prototype (which may be null). An instance of an unregistered class
86
+ // is treated as plain, unless it is kept by reference.
87
+ const keepsProto =
88
+ registeredClassName(obj) !== undefined || isPlainObject(val);
89
+ if (!keepsProto && options.keepUnregistered) return val;
90
+ const copy = (
91
+ keepsProto
92
+ ? Object.create(Object.getPrototypeOf(obj) as object | null)
93
+ : {}
94
+ ) as Record<string, unknown>;
95
+ seen.set(obj, copy);
96
+ for (const key of Object.keys(obj)) {
97
+ setOwn(copy, key, clone((obj as Record<string, unknown>)[key]));
98
+ }
99
+ return copy;
100
+ }
101
+
102
+ return clone(value) as T;
103
+ }
104
+
105
+ // --- Deep Equal ---
106
+
107
+ /**
108
+ * Structural equality over the value types deepClone() supports: primitives,
109
+ * arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
110
+ * at any depth). Map and Set entries are compared in insertion order. Arrays
111
+ * are compared by length and index, so a hole equals an undefined element
112
+ * (deepClone() and save/load turn holes into undefined elements).
113
+ */
114
+ export function deepEqual(a: unknown, b: unknown): boolean {
115
+ return equal(a, b, new Map());
116
+ }
117
+
118
+ /**
119
+ * `assumed` holds the pairs already being compared: meeting one again (a
120
+ * cycle) assumes it equal. Every object may pair with several others, since
121
+ * cycles of different lengths can still unfold to the same value.
122
+ */
123
+ function equal(
124
+ a: unknown,
125
+ b: unknown,
126
+ assumed: Map<object, Set<object>>,
127
+ ): boolean {
128
+ if (Object.is(a, b)) return true;
129
+ if (
130
+ a === null ||
131
+ b === null ||
132
+ typeof a !== 'object' ||
133
+ typeof b !== 'object'
134
+ ) {
135
+ return false;
136
+ }
137
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
138
+ const pairs = assumed.get(a);
139
+ if (pairs?.has(b)) return true;
140
+ if (pairs) pairs.add(b);
141
+ else assumed.set(a, new Set([b]));
142
+
143
+ if (a instanceof Date) return Object.is(a.getTime(), (b as Date).getTime());
144
+ if (a instanceof RegExp) return String(a) === String(b);
145
+ if (a instanceof Map || a instanceof Set) {
146
+ const bc = b as Map<unknown, unknown> | Set<unknown>;
147
+ if (a.size !== bc.size) return false;
148
+ const ai = a.entries();
149
+ const bi = bc.entries();
150
+ for (
151
+ let x = ai.next(), y = bi.next();
152
+ !x.done;
153
+ x = ai.next(), y = bi.next()
154
+ ) {
155
+ if (!equal(x.value, y.value, assumed)) return false;
156
+ }
157
+ return true;
158
+ }
159
+
160
+ if (Array.isArray(a)) {
161
+ const bc = b as unknown[];
162
+ if (a.length !== bc.length) return false;
163
+ for (let i = 0; i < a.length; i++) {
164
+ if (!equal(a[i], bc[i], assumed)) return false;
165
+ }
166
+ return true;
167
+ }
168
+
169
+ const ao = a as Record<string, unknown>;
170
+ const bo = b as Record<string, unknown>;
171
+ const keys = Object.keys(ao);
172
+ if (keys.length !== Object.keys(bo).length) return false;
173
+ for (const key of keys) {
174
+ if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
175
+ }
176
+ return true;
177
+ }
178
+
179
+ // --- Structural diff ---
180
+
181
+ /**
182
+ * Objects merged property by property: plain objects and (registered)
183
+ * class instances. Arrays, Map, Set, Date and RegExp are values that are
184
+ * replaced as a whole, since their elements have no stable identity to
185
+ * merge by (a shift moves every index).
186
+ */
187
+ export function isMergeable(value: unknown): value is Record<string, unknown> {
188
+ return (
189
+ typeof value === 'object' &&
190
+ value !== null &&
191
+ !Array.isArray(value) &&
192
+ !(value instanceof Map) &&
193
+ !(value instanceof Set) &&
194
+ !(value instanceof Date) &&
195
+ !(value instanceof RegExp)
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Whether changes between `a` and `b` merge key by key: both are objects of
201
+ * one class (see isMergeable), or, with `arrays`, both are arrays.
202
+ */
203
+ export function mergesWith(
204
+ a: unknown,
205
+ b: unknown,
206
+ arrays: boolean,
207
+ ): a is Record<string, unknown> {
208
+ if (arrays && (Array.isArray(a) || Array.isArray(b))) {
209
+ return Array.isArray(a) && Array.isArray(b);
210
+ }
211
+ return (
212
+ isMergeable(a) &&
213
+ isMergeable(b) &&
214
+ Object.getPrototypeOf(a) === Object.getPrototypeOf(b)
215
+ );
216
+ }
217
+
218
+ /** How a value differs from an earlier version at one key. */
219
+ export type KeyChange =
220
+ | { key: string; kind: 'added' | 'changed'; after: unknown }
221
+ | { key: string; kind: 'deleted' }
222
+ | {
223
+ key: string;
224
+ kind: 'nested';
225
+ before: Record<string, unknown>;
226
+ after: Record<string, unknown>;
227
+ };
228
+
229
+ /**
230
+ * The keys at which `after` differs from `before`, two values that merge
231
+ * (see mergesWith): keys added, deleted, or holding another value. Store
232
+ * updates are immutable, so a value that kept its identity is no change,
233
+ * and one rebuilt with equal content (mutation code commits whole values)
234
+ * is none either. Values that merge are reported as 'nested', to be
235
+ * compared key by key; they may still be equal. With `arrays`, arrays
236
+ * merge too: elements at the indices both hold are compared, and the
237
+ * elements one has beyond the other's length are added or deleted.
238
+ */
239
+ export function keyChanges(
240
+ before: Record<string, unknown>,
241
+ after: Record<string, unknown>,
242
+ arrays: boolean,
243
+ ): KeyChange[] {
244
+ const changes: KeyChange[] = [];
245
+ const compare = (key: string, b: unknown, a: unknown): void => {
246
+ if (Object.is(b, a)) return;
247
+ if (mergesWith(b, a, arrays)) {
248
+ changes.push({
249
+ key,
250
+ kind: 'nested',
251
+ before: b,
252
+ after: a as Record<string, unknown>,
253
+ });
254
+ } else if (!deepEqual(b, a)) {
255
+ changes.push({ key, kind: 'changed', after: a });
256
+ }
257
+ };
258
+
259
+ if (Array.isArray(after)) {
260
+ const b = before as unknown as unknown[];
261
+ const a = after as unknown[];
262
+ for (let i = 0; i < Math.min(b.length, a.length); i++) {
263
+ compare(String(i), b[i], a[i]);
264
+ }
265
+ for (let i = b.length; i < a.length; i++) {
266
+ changes.push({ key: String(i), kind: 'added', after: a[i] });
267
+ }
268
+ for (let i = a.length; i < b.length; i++) {
269
+ changes.push({ key: String(i), kind: 'deleted' });
270
+ }
271
+ return changes;
272
+ }
273
+
274
+ for (const key of Object.keys(after)) {
275
+ if (hasOwn(before, key)) compare(key, before[key], after[key]);
276
+ else changes.push({ key, kind: 'added', after: after[key] });
277
+ }
278
+ for (const key of Object.keys(before)) {
279
+ if (!hasOwn(after, key)) changes.push({ key, kind: 'deleted' });
280
+ }
281
+ return changes;
282
+ }
283
+
284
+ /** A write to one property path of an object, or its deletion. */
285
+ export type PathChange =
286
+ | { path: string[]; deleted: false; value: unknown }
287
+ | { path: string[]; deleted: true };
288
+
289
+ /**
290
+ * The property paths where `after` differs from `before`, two objects of
291
+ * one class: objects that merge (see isMergeable) are compared property by
292
+ * property, any other value as a whole.
293
+ */
294
+ export function diffPaths(
295
+ before: Record<string, unknown>,
296
+ after: Record<string, unknown>,
297
+ ): PathChange[] {
298
+ const changes: PathChange[] = [];
299
+ const ancestors = new Set<object>();
300
+ (function walk(
301
+ b: Record<string, unknown>,
302
+ a: Record<string, unknown>,
303
+ path: string[],
304
+ ): void {
305
+ // Stop at cycles; shared (non-cyclic) references are visited per path.
306
+ if (ancestors.has(a)) return;
307
+ ancestors.add(a);
308
+ for (const change of keyChanges(b, a, false)) {
309
+ const at = [...path, change.key];
310
+ if (change.kind === 'nested') {
311
+ walk(change.before, change.after, at);
312
+ } else {
313
+ changes.push(
314
+ change.kind === 'deleted'
315
+ ? { path: at, deleted: true }
316
+ : { path: at, deleted: false, value: change.after },
317
+ );
318
+ }
319
+ }
320
+ ancestors.delete(a);
321
+ })(before, after, []);
322
+ return changes;
323
+ }
324
+
325
+ /** Whether `target` already holds what `change` would write. */
326
+ export function isApplied(
327
+ target: Record<string, unknown>,
328
+ change: PathChange,
329
+ ): boolean {
330
+ const parent = getByPath(target, change.path.slice(0, -1));
331
+ if (parent === null || typeof parent !== 'object') return change.deleted;
332
+ const key = change.path[change.path.length - 1]!;
333
+ if (!hasOwn(parent, key)) return change.deleted;
334
+ return (
335
+ !change.deleted &&
336
+ deepEqual((parent as Record<string, unknown>)[key], change.value)
337
+ );
338
+ }
339
+
340
+ export function applyChange(
341
+ target: Record<string, unknown>,
342
+ change: PathChange,
343
+ ): void {
344
+ if (change.deleted) deleteByPath(target, change.path);
345
+ else setByPath(target, change.path, change.value);
346
+ }
347
+
348
+ /**
349
+ * Write how `after` differs from `before` (see keyChanges; arrays merge by
350
+ * index) into `target`, another version of the same object or array.
351
+ * Values that changed are written whole, as deep copies; for values that
352
+ * merge, `mergeNested` may merge them into `target` instead, returning
353
+ * whether it did. Into an array, elements removed from the end are removed
354
+ * at the same indices and elements added are appended, and changes at
355
+ * indices `target` lacks are dropped: where `target` was resized, its
356
+ * indices do not line up with `before`'s.
357
+ */
358
+ export function mergeKeys(
359
+ target: Record<string, unknown>,
360
+ before: Record<string, unknown>,
361
+ after: Record<string, unknown>,
362
+ mergeNested?: (
363
+ key: string,
364
+ before: Record<string, unknown>,
365
+ after: Record<string, unknown>,
366
+ ) => boolean,
367
+ ): void {
368
+ const list = Array.isArray(target) ? (target as unknown[]) : undefined;
369
+ for (const change of keyChanges(before, after, true)) {
370
+ const { key } = change;
371
+ if (change.kind === 'deleted') {
372
+ if (list) list.length = Math.min(list.length, Number(key));
373
+ else delete target[key];
374
+ } else if (list && change.kind === 'added') {
375
+ list.push(deepClone(change.after));
376
+ } else if (list && Number(key) >= list.length) {
377
+ continue;
378
+ } else if (
379
+ change.kind !== 'nested' ||
380
+ !(
381
+ mergeNested?.(key, change.before, change.after) ||
382
+ deepEqual(change.before, change.after)
383
+ )
384
+ ) {
385
+ target[key] = deepClone(change.after);
386
+ }
387
+ }
388
+ }
package/types/index.d.ts CHANGED
@@ -239,6 +239,35 @@ export interface StorageQuota {
239
239
  estimateSupported: boolean;
240
240
  }
241
241
 
242
+ /**
243
+ * How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
244
+ * `\'` and `\\` escapes.
245
+ * - `expression`: code, as written (the default).
246
+ * - `variable`: a variable reference such as `$name` or `"$name"`, as written.
247
+ * - `string`: one quoted string; anything else leaves the argument unset.
248
+ * - `text`: one quoted string, or text with any loose quotes stripped.
249
+ * - `names`: a comma-separated list of names (`@item, @i`).
250
+ * - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
251
+ * - `number`: a number.
252
+ * - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
253
+ * - `separator`: a word (`of`) or `=` separating the parameters before it
254
+ * from those after it; a boolean.
255
+ * - `options`: keywords, the names of its `parameters`, each followed by a
256
+ * quoted string or a number unless it is a flag.
257
+ * @see {@link ../../src/registry.ts} for the implementation.
258
+ */
259
+ export type ParameterType =
260
+ | 'expression'
261
+ | 'variable'
262
+ | 'string'
263
+ | 'text'
264
+ | 'names'
265
+ | 'delay'
266
+ | 'number'
267
+ | 'flag'
268
+ | 'separator'
269
+ | 'options';
270
+
242
271
  /**
243
272
  * Parameter metadata for a macro definition.
244
273
  * @see {@link ../../src/registry.ts} for the implementation.
@@ -247,8 +276,32 @@ export interface ParameterDef {
247
276
  name: string;
248
277
  required?: boolean;
249
278
  description?: string;
279
+ /** How the argument is read (default `expression`). */
280
+ type?: ParameterType;
281
+ /** The options of an `options` parameter. */
282
+ parameters?: readonly ParameterDef[];
250
283
  }
251
284
 
285
+ type ArgValue<T, D> = T extends 'flag' | 'separator'
286
+ ? boolean
287
+ : T extends 'names'
288
+ ? string[] | undefined
289
+ : T extends 'delay' | 'number'
290
+ ? number | undefined
291
+ : T extends 'options'
292
+ ? D extends { parameters: infer Q extends readonly ParameterDef[] }
293
+ ? Partial<MacroArgs<Q>>
294
+ : Partial<MacroArgs>
295
+ : string | undefined;
296
+
297
+ /**
298
+ * A macro's arguments (`ctx.args`), by parameter name.
299
+ * @see {@link ../../src/registry.ts} for the implementation.
300
+ */
301
+ export type MacroArgs<P extends readonly ParameterDef[] = ParameterDef[]> = {
302
+ [D in P[number] as D['name']]: ArgValue<D['type'], D>;
303
+ };
304
+
252
305
  /**
253
306
  * Metadata about a registered macro, returned by `Story.getMacroRegistry()`.
254
307
  * @see {@link ../../src/registry.ts} for the implementation.
@@ -363,7 +416,9 @@ export interface UseActionOptions {
363
416
  * this package), so `ctx.hooks.useState<T>()`, `ctx.h()` etc. are fully typed.
364
417
  * @see {@link ../../src/define-macro.ts} for the implementation.
365
418
  */
366
- export interface MacroContext {
419
+ export interface MacroContext<A = MacroArgs> {
420
+ /** The arguments, read into the macro's declared `parameters`. */
421
+ args: A;
367
422
  className?: string;
368
423
  id?: string;
369
424
  resolve?: (s: string | undefined) => string | undefined;
@@ -437,7 +492,9 @@ export interface MacroTextContext {
437
492
  * Configuration object for `Story.defineMacro()`.
438
493
  * @see {@link ../../src/define-macro.ts} for the implementation.
439
494
  */
440
- export interface MacroDefinition {
495
+ export interface MacroDefinition<
496
+ P extends readonly ParameterDef[] = ParameterDef[],
497
+ > {
441
498
  name: string;
442
499
  /** Sub-macro names (e.g. `['option']`); a non-empty list makes the macro a block macro. */
443
500
  subMacros?: string[];
@@ -451,15 +508,21 @@ export interface MacroDefinition {
451
508
  storeVar?: boolean;
452
509
  /** Tooling hint: one-line description shown by editors. */
453
510
  description?: string;
454
- /** Tooling hint: positional parameters. */
455
- parameters?: ParameterDef[];
456
- render: (props: MacroProps, ctx: MacroContext) => ComponentChildren;
511
+ /** The parameters: read into `ctx.args`, and shown by tooling. */
512
+ parameters?: P;
513
+ render: (
514
+ props: MacroProps,
515
+ ctx: MacroContext<MacroArgs<P>>,
516
+ ) => ComponentChildren;
457
517
  /**
458
518
  * The macro's text form, used where markup becomes a string: HTML
459
519
  * attribute values, image alt text and link titles, macro labels. Without
460
520
  * one, the macro can't be used there and is reported as an error.
461
521
  */
462
- text?: (props: MacroProps, ctx: MacroTextContext) => string;
522
+ text?: (
523
+ props: MacroProps,
524
+ ctx: MacroTextContext & { args: MacroArgs<P> },
525
+ ) => string;
463
526
  }
464
527
 
465
528
  /**
@@ -1,7 +1,39 @@
1
+ /**
2
+ * How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
3
+ * `\'` and `\\` escapes.
4
+ * - `expression`: code, as written (the default).
5
+ * - `variable`: a variable reference such as `$name` or `"$name"`, as written.
6
+ * - `string`: one quoted string; anything else leaves the argument unset.
7
+ * - `text`: one quoted string, or text with any loose quotes stripped.
8
+ * - `names`: a comma-separated list of names (`@item, @i`).
9
+ * - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
10
+ * - `number`: a number.
11
+ * - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
12
+ * - `separator`: a word (`of`) or `=` separating the parameters before it
13
+ * from those after it; a boolean.
14
+ * - `options`: keywords, the names of its `parameters`, each followed by a
15
+ * quoted string or a number unless it is a flag.
16
+ */
17
+ export type ParameterType =
18
+ | 'expression'
19
+ | 'variable'
20
+ | 'string'
21
+ | 'text'
22
+ | 'names'
23
+ | 'delay'
24
+ | 'number'
25
+ | 'flag'
26
+ | 'separator'
27
+ | 'options';
28
+
1
29
  export interface ParameterDef {
2
30
  name: string;
3
31
  required?: boolean;
4
32
  description?: string;
33
+ /** How the argument is read (default `expression`). */
34
+ type?: ParameterType;
35
+ /** The options of an `options` parameter. */
36
+ parameters?: readonly ParameterDef[];
5
37
  }
6
38
 
7
39
  export interface MacroMetadata {
@@ -24,7 +56,7 @@ export interface MacroDefinition {
24
56
  merged?: boolean;
25
57
  storeVar?: boolean;
26
58
  description?: string;
27
- parameters?: ParameterDef[];
59
+ parameters?: readonly ParameterDef[];
28
60
  render: (...args: any[]) => any;
29
61
  text?: (...args: any[]) => string;
30
62
  }