gesso-vite-plugin 0.6.30 → 0.8.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.
@@ -0,0 +1,1299 @@
1
+ //#region src/catalog.ts
2
+ /** What tells an agent, when it connects, that the catalog is here and to use it. */
3
+ const CATALOG_INSTRUCTIONS = "This project has its own components. Before building anything, read them: library_overview lists every one, library_search finds the ones that fit, and library_get gives one in full with the inputs it takes. Use them, and write the theme's names from tokens_view rather than raw colours and sizes.";
4
+ const READ_ONLY = {
5
+ readOnlyHint: true,
6
+ openWorldHint: false
7
+ };
8
+ const CATALOG_TOOLS = [
9
+ {
10
+ name: "library_overview",
11
+ title: "List the project's components",
12
+ description: "Every component this project has, one line each: its name, what it is for, where to import it from and how many files use it. Read it before building anything, and use what is here.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ additionalProperties: false
17
+ },
18
+ annotations: READ_ONLY
19
+ },
20
+ {
21
+ name: "library_search",
22
+ title: "Find components",
23
+ description: "The components whose name, purpose or inputs match a few words, such as \"date range\" or \"avatar\". Best matches first.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: { query: {
27
+ type: "string",
28
+ description: "A few words for what the component should be or do."
29
+ } },
30
+ required: ["query"],
31
+ additionalProperties: false
32
+ },
33
+ annotations: READ_ONLY
34
+ },
35
+ {
36
+ name: "library_get",
37
+ title: "Read a component",
38
+ description: "One component in full: what it is for, every input it takes with its type, default and meaning, examples, and the files that already use it, which show how this project writes it.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ name: {
43
+ type: "string",
44
+ description: "The component, as library_overview names it."
45
+ },
46
+ from: {
47
+ type: "string",
48
+ description: "Where it is imported from, when two components share the name."
49
+ }
50
+ },
51
+ required: ["name"],
52
+ additionalProperties: false
53
+ },
54
+ annotations: READ_ONLY
55
+ }
56
+ ];
57
+ var ComponentCatalog = class {
58
+ cached = null;
59
+ reader;
60
+ root;
61
+ options;
62
+ constructor(reader, root, options) {
63
+ this.reader = reader;
64
+ this.root = root.replace(/\/+$/, "");
65
+ this.options = options;
66
+ }
67
+ /**
68
+ * Something in the project changed. The whole catalog is read again
69
+ * on the next call: a project's components read in a tenth of a
70
+ * second, and a save can move a component between files, rename it,
71
+ * or start using it somewhere else.
72
+ */
73
+ invalidate() {
74
+ this.cached = null;
75
+ }
76
+ /**
77
+ * The components, read once until something changes. A read that
78
+ * fails (TypeScript refusing a project mid-change) is a warning and an
79
+ * empty catalog, tried again on the next call, never a rejection left
80
+ * for nobody to catch, which ends the dev server.
81
+ */
82
+ entries() {
83
+ return this.cached ??= this.read().catch((error) => {
84
+ this.cached = null;
85
+ console.warn(`[gesso] The component catalog could not be read: ${String(error.message ?? error)}`);
86
+ return [];
87
+ });
88
+ }
89
+ async call(name, args = {}) {
90
+ if (await this.reader() === null) return failure("The catalog needs TypeScript 7, which reads the components. Add `typescript` (7 or later).");
91
+ const entries = await this.entries();
92
+ switch (name) {
93
+ case "library_overview": return success(formatOverview(entries), { components: entries.map(summaryOf) });
94
+ case "library_search": {
95
+ if (typeof args.query !== "string" || args.query.trim() === "") return failure("library_search needs query: a few words for what the component should be or do.");
96
+ const found = search(entries, args.query);
97
+ if (found.length === 0) return success(`Nothing matches "${args.query}". library_overview lists every component.`, { components: [] });
98
+ return success(formatLines(found), { components: found.map(summaryOf) });
99
+ }
100
+ case "library_get": {
101
+ if (typeof args.name !== "string") return failure("library_get needs name: the component, as library_overview names it.");
102
+ const named = entries.filter((entry) => entry.name === args.name);
103
+ const matches = typeof args.from === "string" ? named.filter((entry) => entry.from === args.from) : named;
104
+ if (matches.length === 0) {
105
+ const near = search(entries, args.name).slice(0, 3);
106
+ return failure(`There is no component called ${args.name}` + (near.length === 0 ? "." : `. Did you mean ${near.map((entry) => entry.name).join(", ")}?`));
107
+ }
108
+ if (matches.length > 1) return failure(`${matches.length} components are called ${args.name}; say which with from: ` + matches.map((entry) => JSON.stringify(entry.from)).join(", "));
109
+ const entry = matches[0];
110
+ return success(formatEntry(entry), {
111
+ ...entry,
112
+ schema: entry.schema
113
+ });
114
+ }
115
+ default: return failure(`There is no tool called ${name}.`);
116
+ }
117
+ }
118
+ async read() {
119
+ const reader = await this.reader();
120
+ if (reader === null) return [];
121
+ const excluded = (this.options.exclude ?? []).map((folder) => `${this.root}/${folder.replace(/^\.?\/+|\/+$/g, "")}/`);
122
+ const sources = reader.sources(this.options.tsconfig).filter((source) => source.file.startsWith(`${this.root}/`) && !/\.(?:spec|test)\.[cm]?[jt]sx?$/.test(source.file) && !excluded.some((folder) => source.file.startsWith(folder)));
123
+ const found = [];
124
+ for (const source of sources) {
125
+ if (!mayDeclareComponent(source.text)) continue;
126
+ const from = source.file.slice(this.root.length + 1).replace(/\.[cm]?[jt]sx?$/, "");
127
+ for (const [name, schema] of reader.readComponents(source.file, this.options.tsconfig).components) found.push({
128
+ name,
129
+ from,
130
+ file: source.file,
131
+ schema
132
+ });
133
+ }
134
+ for (const library of await this.options.libraries?.() ?? []) for (const [name, schema] of reader.readComponents(library.file).components) found.push({
135
+ name,
136
+ from: library.name,
137
+ library: library.name,
138
+ schema
139
+ });
140
+ return found.map((component) => {
141
+ const tag = new RegExp(`<${component.name}[\\s/>]`);
142
+ const usedIn = sources.filter((source) => source.file !== component.file && tag.test(source.text)).map((source) => source.file.slice(this.root.length + 1));
143
+ return {
144
+ name: component.name,
145
+ from: component.from,
146
+ ...component.library === void 0 ? {} : { library: component.library },
147
+ schema: component.schema,
148
+ usedIn
149
+ };
150
+ });
151
+ }
152
+ };
153
+ /**
154
+ * Whether a module could declare a component, without asking the
155
+ * checker: it imports from `gesso-framework` and names `Inputs` or
156
+ * `Component`. Most of a project's files are neither, and each one the
157
+ * checker is spared is a round trip to its process.
158
+ */
159
+ function mayDeclareComponent(text) {
160
+ return text.includes("gesso-framework") && /\b(?:Inputs|Component)\b/.test(text);
161
+ }
162
+ function summaryOf(entry) {
163
+ return {
164
+ name: entry.name,
165
+ from: entry.from,
166
+ ...entry.library === void 0 ? {} : { library: entry.library },
167
+ ...entry.schema.summary === void 0 ? {} : { summary: entry.schema.summary },
168
+ ...entry.schema.deprecated === void 0 ? {} : { deprecated: entry.schema.deprecated },
169
+ usedIn: entry.usedIn.length
170
+ };
171
+ }
172
+ function formatOverview(entries) {
173
+ if (entries.length === 0) return "This project has no components yet: no module exports a function taking Inputs<P>, or a class extending Component. Build the first ones, and they will be listed here.";
174
+ const own = entries.filter((entry) => entry.library === void 0);
175
+ return [...[`${own.length} component${own.length === 1 ? "" : "s"} in this project` + (own.length === entries.length ? "" : `, and ${entries.length - own.length} from libraries it names`) + ". library_get gives one in full.", ""], formatLines(entries)].join("\n");
176
+ }
177
+ /** One line per component, grouped under where each is imported from. */
178
+ function formatLines(entries) {
179
+ const groups = /* @__PURE__ */ new Map();
180
+ for (const entry of entries) {
181
+ const group = groups.get(entry.from) ?? [];
182
+ group.push(entry);
183
+ groups.set(entry.from, group);
184
+ }
185
+ const lines = [];
186
+ for (const [from, group] of groups) {
187
+ lines.push(group[0].library === void 0 ? from : `${from} (library)`);
188
+ for (const entry of group) {
189
+ const summary = firstSentence(entry.schema.summary);
190
+ const deprecated = entry.schema.deprecated === void 0 ? "" : " [deprecated]";
191
+ const used = entry.usedIn.length === 0 ? "" : ` (used in ${entry.usedIn.length})`;
192
+ lines.push(` ${entry.name}${deprecated}${summary === "" ? "" : `: ${summary}`}${used}`);
193
+ }
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+ /**
198
+ * One component as an agent writing TSX wants it: the import, each
199
+ * input as `name type = default` with its meaning under it, the shared
200
+ * inputs folded into a line, and the files to copy its use from. The schema itself
201
+ * goes with it as structured content, for a client that validates.
202
+ */
203
+ function formatEntry(entry) {
204
+ const schema = entry.schema;
205
+ const lines = [`${entry.name}, from ${entry.from}${entry.library === void 0 ? "" : " (library)"}`];
206
+ if (schema.deprecated !== void 0) lines.push(`Deprecated${schema.deprecated === true ? "." : `: ${schema.deprecated}`}`);
207
+ if (schema.summary !== void 0) lines.push(schema.summary);
208
+ const inputs = schema.inputs;
209
+ const required = new Set(inputs.required ?? []);
210
+ const own = [];
211
+ const shared = /* @__PURE__ */ new Map();
212
+ for (const [name, input] of Object.entries(inputs.properties ?? {})) {
213
+ const from = input["x-gesso-from"];
214
+ if (typeof from === "string") {
215
+ shared.set(from, [...shared.get(from) ?? [], name]);
216
+ continue;
217
+ }
218
+ own.push(` ${name}${required.has(name) ? "*" : ""} ${describeInput(input)}`);
219
+ }
220
+ lines.push("", own.length === 0 ? "Inputs: none of its own." : "Inputs (* required):", ...own);
221
+ for (const [from, names] of shared) lines.push(`Also takes the inputs of ${from}: ${names.join(", ")}.`);
222
+ if (entry.usedIn.length > 0) lines.push("", `Used in: ${entry.usedIn.join(", ")}`);
223
+ if (schema.examples !== void 0) lines.push("", "Examples:", ...schema.examples.map((example) => indent(example)));
224
+ if (schema.description !== void 0 && schema.description.replace(/\s+/g, " ").trim() !== schema.summary) lines.push("", "About it:", indent(schema.description));
225
+ return lines.join("\n");
226
+ }
227
+ function describeInput(input) {
228
+ const type = Array.isArray(input.enum) && input.enum.length > 0 ? input.enum.map((value) => JSON.stringify(value)).join(" | ") : String(input["x-gesso-type"] ?? "unknown");
229
+ const kind = input["x-gesso-callback"] === true ? " (callback)" : input["x-gesso-child"] === true ? " (child)" : "";
230
+ const fallback = input.default === void 0 ? "" : ` = ${JSON.stringify(input.default)}`;
231
+ const meaning = firstParagraph(typeof input.description === "string" ? input.description : void 0);
232
+ return `${type}${kind}${fallback}${meaning === "" ? "" : `\n ${meaning}`}`;
233
+ }
234
+ /**
235
+ * The components a few words find, best first: a word in the name
236
+ * counts most, then in the summary, then in the rest of the description
237
+ * and the input names. A component every word misses is left out.
238
+ */
239
+ function search(entries, query) {
240
+ const words = query.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
241
+ return entries.map((entry) => {
242
+ const name = spaced(entry.name);
243
+ const summary = (entry.schema.summary ?? "").toLowerCase();
244
+ const rest = `${entry.schema.description ?? ""} ${Object.keys(entry.schema.inputs.properties ?? {}).join(" ")}`.toLowerCase();
245
+ let score = 0;
246
+ for (const word of words) score += name.includes(word) ? 6 : summary.includes(word) ? 3 : rest.includes(word) ? 1 : 0;
247
+ return {
248
+ entry,
249
+ score
250
+ };
251
+ }).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).map(({ entry }) => entry);
252
+ }
253
+ /** `DateRangePicker` as `date range picker datarangepicker`, so both spellings find it. */
254
+ function spaced(name) {
255
+ return `${name.replace(/([a-z0-9])([A-Z])/g, "$1 $2")} ${name}`.toLowerCase();
256
+ }
257
+ function firstSentence(text) {
258
+ if (text === void 0) return "";
259
+ const flat = text.replace(/\s+/g, " ").trim();
260
+ const end = flat.search(/[.!?](\s|$)/);
261
+ const sentence = end === -1 ? flat : flat.slice(0, end + 1);
262
+ return sentence.length > 140 ? `${sentence.slice(0, 139)}…` : sentence;
263
+ }
264
+ function firstParagraph(text) {
265
+ if (text === void 0) return "";
266
+ const paragraph = text.split(/\n\s*\n/)[0].replace(/\s+/g, " ").trim();
267
+ return paragraph.length > 240 ? `${paragraph.slice(0, 239)}…` : paragraph;
268
+ }
269
+ function indent(text) {
270
+ return text.split("\n").map((line) => ` ${line}`).join("\n");
271
+ }
272
+ function success(text, structured) {
273
+ return {
274
+ content: [{
275
+ type: "text",
276
+ text
277
+ }],
278
+ structuredContent: structured,
279
+ isError: false
280
+ };
281
+ }
282
+ function failure(text) {
283
+ return {
284
+ content: [{
285
+ type: "text",
286
+ text
287
+ }],
288
+ isError: true
289
+ };
290
+ }
291
+ //#endregion
292
+ //#region src/source.ts
293
+ const IDENTIFIER = /[A-Za-z0-9_$]/;
294
+ /**
295
+ * A copy of `code` in which every string, template literal, regular
296
+ * expression body and comment is spaces, and everything else is
297
+ * untouched.
298
+ *
299
+ * Same length as the input, and newlines are kept, so an index into
300
+ * the result is an index into the original and a line number is still
301
+ * a line number. String and template delimiters are kept as well, so a
302
+ * specifier can be found by its quotes here and read from there.
303
+ */
304
+ function blankLiterals(code) {
305
+ const out = code.split("");
306
+ let index = 0;
307
+ let regexAllowed = true;
308
+ const blank = (from, to) => {
309
+ for (let i = from; i < to; i++) if (out[i] !== "\n") out[i] = " ";
310
+ };
311
+ while (index < code.length) {
312
+ const char = code[index];
313
+ if (char === "/" && code[index + 1] === "/") {
314
+ const end = code.indexOf("\n", index);
315
+ blank(index, end === -1 ? code.length : end);
316
+ index = end === -1 ? code.length : end;
317
+ continue;
318
+ }
319
+ if (char === "/" && code[index + 1] === "*") {
320
+ const end = code.indexOf("*/", index + 2);
321
+ const stop = end === -1 ? code.length : end + 2;
322
+ blank(index, stop);
323
+ index = stop;
324
+ continue;
325
+ }
326
+ if (char === "\"" || char === "'") {
327
+ const end = closingQuote(code, index, char);
328
+ blank(index + 1, end);
329
+ index = end + 1;
330
+ regexAllowed = false;
331
+ continue;
332
+ }
333
+ if (char === "`") {
334
+ const end = closingQuote(code, index, "`");
335
+ blank(index + 1, end);
336
+ index = end + 1;
337
+ regexAllowed = false;
338
+ continue;
339
+ }
340
+ if (char === "/" && regexAllowed) {
341
+ const end = closingRegex(code, index);
342
+ if (end !== -1) {
343
+ blank(index + 1, end);
344
+ index = end + 1;
345
+ regexAllowed = false;
346
+ continue;
347
+ }
348
+ }
349
+ if (!/\s/.test(char)) regexAllowed = !IDENTIFIER.test(char) && char !== ")" && char !== "]";
350
+ index++;
351
+ }
352
+ return out.join("");
353
+ }
354
+ /** The index of the quote that closes the one at `open`, or the end. */
355
+ function closingQuote(code, open, quote) {
356
+ for (let i = open + 1; i < code.length; i++) {
357
+ if (code[i] === "\\") {
358
+ i++;
359
+ continue;
360
+ }
361
+ if (code[i] === quote) return i;
362
+ }
363
+ return code.length;
364
+ }
365
+ /**
366
+ * The index of the slash closing a regular expression, or -1 when the
367
+ * slash was division after all.
368
+ *
369
+ * A newline inside means it was not a regular expression, which is the
370
+ * cheap test that keeps `a / b` and `c / d` on one line from swallowing
371
+ * everything between them.
372
+ */
373
+ function closingRegex(code, open) {
374
+ let inClass = false;
375
+ for (let i = open + 1; i < code.length; i++) {
376
+ const char = code[i];
377
+ if (char === "\\") {
378
+ i++;
379
+ continue;
380
+ }
381
+ if (char === "\n") return -1;
382
+ if (char === "[") {
383
+ inClass = true;
384
+ continue;
385
+ }
386
+ if (char === "]") {
387
+ inClass = false;
388
+ continue;
389
+ }
390
+ if (char === "/" && !inClass) return i;
391
+ }
392
+ return -1;
393
+ }
394
+ /**
395
+ * Every call of `name` in the module, in source order.
396
+ *
397
+ * A bare name has to stand alone: `renderRoot(` matches, `myRenderRoot(`
398
+ * and `thing.renderRoot(` do not, because a method of that name on some
399
+ * other object is not the framework's function. A name written with a
400
+ * leading dot is the opposite and matches only the method call:
401
+ * `.useService(` is how a builder chain reads and the only way it is
402
+ * ever written.
403
+ */
404
+ function findCalls(code, name, blank = blankLiterals(code)) {
405
+ const member = name.startsWith(".");
406
+ const bare = member ? name.slice(1) : name;
407
+ const sites = [];
408
+ let from = 0;
409
+ for (;;) {
410
+ const at = blank.indexOf(bare, from);
411
+ if (at === -1) return sites;
412
+ from = at + bare.length;
413
+ const before = at === 0 ? "" : blank[at - 1];
414
+ if (member ? before !== "." : before !== "" && (IDENTIFIER.test(before) || before === ".")) continue;
415
+ let cursor = from;
416
+ while (cursor < blank.length && /\s/.test(blank[cursor])) cursor++;
417
+ if (blank[cursor] !== "(") continue;
418
+ const argsEnd = matchingBracket(blank, cursor);
419
+ if (argsEnd === -1) continue;
420
+ sites.push({
421
+ start: at,
422
+ argsStart: cursor + 1,
423
+ argsEnd,
424
+ end: argsEnd + 1
425
+ });
426
+ from = argsEnd + 1;
427
+ }
428
+ }
429
+ /** The first call of `name`, or null. */
430
+ function findCall(code, name, blank = blankLiterals(code)) {
431
+ return findCalls(code, name, blank)[0] ?? null;
432
+ }
433
+ /** The index of the bracket closing the one at `open`, or -1. */
434
+ function matchingBracket(blank, open) {
435
+ const stack = [];
436
+ for (let i = open; i < blank.length; i++) {
437
+ const char = blank[i];
438
+ if (char === "(" || char === "[" || char === "{") {
439
+ stack.push(char);
440
+ continue;
441
+ }
442
+ if (char === ")" || char === "]" || char === "}") {
443
+ stack.pop();
444
+ if (stack.length === 0) return i;
445
+ }
446
+ }
447
+ return -1;
448
+ }
449
+ /**
450
+ * The identifier a call's first argument is, or null when the argument
451
+ * is anything more complicated than a name.
452
+ *
453
+ * `renderRoot(SegueApp)` is the shape every entry in this repository
454
+ * writes and the only one that can be hot-replaced, because a
455
+ * replacement has to be looked up by name in the module that changed.
456
+ * `renderRoot(<App x={1} />)` is legal and gets no HMR wiring, which
457
+ * the plugin says rather than guessing.
458
+ */
459
+ function firstArgumentName(code, call, blank = blankLiterals(code)) {
460
+ const first = (splitTopLevel(blank.slice(call.argsStart, call.argsEnd))[0] ?? "").trim();
461
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(first) ? first : null;
462
+ }
463
+ /** Splits an argument list on its top-level commas. */
464
+ function splitTopLevel(text) {
465
+ const parts = [];
466
+ let depth = 0;
467
+ let start = 0;
468
+ for (let i = 0; i < text.length; i++) {
469
+ const char = text[i];
470
+ if (char === "(" || char === "[" || char === "{") depth++;
471
+ else if (char === ")" || char === "]" || char === "}") depth--;
472
+ else if (char === "," && depth === 0) {
473
+ parts.push(text.slice(start, i));
474
+ start = i + 1;
475
+ }
476
+ }
477
+ parts.push(text.slice(start));
478
+ return parts;
479
+ }
480
+ /**
481
+ * Which module each imported name came from: local name to specifier,
482
+ * exactly as the import wrote it.
483
+ *
484
+ * Default and namespace imports are recorded under their local names
485
+ * too, so `import App from './App'` and `import * as app from './App'`
486
+ * both answer. What the map does not carry is whether the binding is a
487
+ * type, because `import type` is stripped before this runs in every
488
+ * configuration that matters and a type never reaches a call.
489
+ */
490
+ function importSources(code, blank = blankLiterals(code)) {
491
+ const sources = /* @__PURE__ */ new Map();
492
+ const statement = /\bimport\b([\s\S]*?)\bfrom\b\s*(['"])/g;
493
+ for (;;) {
494
+ const match = statement.exec(blank);
495
+ if (match === null) return sources;
496
+ const quote = match[2];
497
+ const open = match.index + match[0].length;
498
+ const close = code.indexOf(quote, open);
499
+ if (close === -1) continue;
500
+ const specifier = code.slice(open, close);
501
+ for (const local of importedNames(match[1])) sources.set(local, specifier);
502
+ statement.lastIndex = close + 1;
503
+ }
504
+ }
505
+ /** The local names an import clause binds. */
506
+ function importedNames(clause) {
507
+ const names = [];
508
+ const braces = clause.indexOf("{");
509
+ const head = (braces === -1 ? clause : clause.slice(0, braces)).replace(/\btype\b/g, "");
510
+ for (const part of head.split(",")) {
511
+ const named = /^\s*(?:\*\s*as\s*)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
512
+ if (named !== null) names.push(named[1]);
513
+ }
514
+ if (braces !== -1) {
515
+ const close = clause.lastIndexOf("}");
516
+ const list = clause.slice(braces + 1, close === -1 ? clause.length : close);
517
+ for (const part of list.split(",")) {
518
+ const aliased = /^\s*(?:type\s+)?[A-Za-z_$][A-Za-z0-9_$]*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
519
+ const plain = /^\s*(?:type\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
520
+ const name = aliased?.[1] ?? plain?.[1];
521
+ if (name !== void 0) names.push(name);
522
+ }
523
+ }
524
+ return names;
525
+ }
526
+ //#endregion
527
+ //#region src/contracts.ts
528
+ /** The functions that declare a channel. */
529
+ const DECLARATIONS = ["channel", "defineChannel"];
530
+ /**
531
+ * Whether a module declares a channel, without asking the checker.
532
+ *
533
+ * The checker is a process round trip per question, and every module
534
+ * the application has passes through `transform`, so this is the gate:
535
+ * a call to `channel` or `defineChannel` imported from
536
+ * `gesso-framework`. A module that re-exports a token someone else
537
+ * declared is not a contract, and the module that declared it is the
538
+ * one described.
539
+ */
540
+ function declaresChannel(code) {
541
+ if (!code.includes("gesso-framework")) return false;
542
+ const blank = blankLiterals(code);
543
+ const imports = importSources(code, blank);
544
+ return DECLARATIONS.some((name) => imports.get(name) === "gesso-framework" && (findCalls(code, name, blank).length > 0 || new RegExp(`(?<![\\w$.])${name}\\s*<`).test(blank)));
545
+ }
546
+ /**
547
+ * The code appended to a contract module.
548
+ *
549
+ * An import at the bottom is still hoisted, and appending rather than
550
+ * rewriting leaves every line the author wrote where they wrote it, so
551
+ * the module's source map needs nothing.
552
+ */
553
+ function describeCalls(channels) {
554
+ const lines = ["import { describeChannel as __gessoDescribeChannel } from 'gesso-framework';"];
555
+ for (const [name, schema] of channels) lines.push(`__gessoDescribeChannel(${name}, ${JSON.stringify(schema)});`);
556
+ return `\n${lines.join("\n")}\n`;
557
+ }
558
+ /**
559
+ * A module that describes channels when it is imported, for a bundle
560
+ * this plugin never sees.
561
+ *
562
+ * The plugin describes a contract by appending to it as Vite bundles
563
+ * it. An Electrobun main process is bundled by Electrobun's own build,
564
+ * which takes no plugins, so there the description is written ahead of
565
+ * time instead: `gesso-channels` reads the contracts with the same
566
+ * checker and writes this module, and the main process imports it once.
567
+ * A token is imported from its contract, so the schema lands on the
568
+ * same object the application serves.
569
+ *
570
+ * `outFile` and every `file` are absolute POSIX paths.
571
+ */
572
+ function describedModule(contracts, outFile) {
573
+ const lines = [
574
+ "// Written by gesso-channels from the channel contracts it names below.",
575
+ "// Run it again after changing a contract rather than editing this file.",
576
+ "import { describeChannel } from 'gesso-framework';"
577
+ ];
578
+ const from = outFile.slice(0, outFile.lastIndexOf("/"));
579
+ for (const contract of contracts) if (contract.channels.size > 0) {
580
+ const names = [...contract.channels.keys()].join(", ");
581
+ lines.push(`import { ${names} } from '${importPath(from, contract.file)}';`);
582
+ }
583
+ for (const contract of contracts) for (const [name, schema] of contract.channels) lines.push("", `describeChannel(${name}, ${JSON.stringify(schema, null, 2)});`);
584
+ return `${lines.join("\n")}\n`;
585
+ }
586
+ /** `./Counter` from `/app/src/shared` to `/app/src/shared/Counter.ts`, without the extension. */
587
+ function importPath(fromDir, file) {
588
+ const from = fromDir.split("/").filter(Boolean);
589
+ const to = file.replace(/\.[cm]?[jt]sx?$/, "").split("/").filter(Boolean);
590
+ let shared = 0;
591
+ while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
592
+ const up = from.length - shared;
593
+ const rest = to.slice(shared).join("/");
594
+ return up === 0 ? `./${rest}` : `${"../".repeat(up)}${rest}`;
595
+ }
596
+ /**
597
+ * The checker, held open across reads.
598
+ *
599
+ * Spawning it and loading a project costs about a hundred
600
+ * milliseconds; a question after that costs well under one. So one
601
+ * reader lives as long as the dev server or the build, is told which
602
+ * files changed between reads, and is closed at the end, because the
603
+ * process it holds would otherwise keep the build from exiting.
604
+ */
605
+ var ContractReader = class {
606
+ api = null;
607
+ changed = /* @__PURE__ */ new Set();
608
+ created = /* @__PURE__ */ new Set();
609
+ deleted = /* @__PURE__ */ new Set();
610
+ /** Each contract's tsconfig, found once. */
611
+ configs = /* @__PURE__ */ new Map();
612
+ /**
613
+ * @param fs Callbacks the checker reads through before the disk, for
614
+ * a spec that keeps its project in memory.
615
+ */
616
+ ts;
617
+ cwd;
618
+ fs;
619
+ constructor(ts, cwd, fs) {
620
+ this.ts = ts;
621
+ this.cwd = cwd;
622
+ this.fs = fs;
623
+ }
624
+ /**
625
+ * A file changed on disk since the last read. A new or deleted one is
626
+ * said so, because a program's list of files only changes for those:
627
+ * a component in a new file is otherwise not in the catalog.
628
+ */
629
+ invalidate(file, event = "update") {
630
+ (event === "create" ? this.created : event === "delete" ? this.deleted : this.changed).add(file);
631
+ }
632
+ /**
633
+ * A snapshot of the projects, with what changed since the last one.
634
+ *
635
+ * The checker keeps every project it has opened, and asked to update
636
+ * one whose tsconfig has since been deleted it refuses the whole
637
+ * update ("project not found for update"). That was a dev server gone
638
+ * with it: a project's tsconfig removed while the server ran threw out
639
+ * of the catalog's read and took the process down. A refused update is
640
+ * a checker started afresh, which reads the disk as it now is and
641
+ * opens only what is asked of it again.
642
+ */
643
+ snapshot(request) {
644
+ const api = this.api ??= new this.ts.API({
645
+ cwd: this.cwd,
646
+ ...this.fs === void 0 ? {} : { fs: this.fs }
647
+ });
648
+ try {
649
+ return api.updateSnapshot({
650
+ ...request,
651
+ fileChanges: this.takeChanges()
652
+ });
653
+ } catch {
654
+ this.close();
655
+ this.configs.clear();
656
+ const fresh = this.api = new this.ts.API({
657
+ cwd: this.cwd,
658
+ ...this.fs === void 0 ? {} : { fs: this.fs }
659
+ });
660
+ const { fileChanges: _changes, ...rest } = request;
661
+ return fresh.updateSnapshot(rest);
662
+ }
663
+ }
664
+ /** What changed since the last snapshot, emptied for the next. */
665
+ takeChanges() {
666
+ const changes = {
667
+ changed: [...this.changed],
668
+ created: [...this.created],
669
+ deleted: [...this.deleted]
670
+ };
671
+ this.changed.clear();
672
+ this.created.clear();
673
+ this.deleted.clear();
674
+ return changes;
675
+ }
676
+ /** Every channel `file` exports, described. */
677
+ read(file) {
678
+ const opened = this.open(file);
679
+ if (typeof opened === "string") return {
680
+ channels: /* @__PURE__ */ new Map(),
681
+ warnings: [opened]
682
+ };
683
+ const { checker, exports } = opened;
684
+ const describer = new Describer(this.ts, checker);
685
+ const channels = /* @__PURE__ */ new Map();
686
+ for (const exported of exports) {
687
+ const type = checker.getTypeOfSymbol(exported);
688
+ if (type === void 0 || type.getSymbol()?.name !== "ChannelToken") continue;
689
+ const [view, commands] = checker.getTypeArguments(type);
690
+ channels.set(exported.name, describer.channel(exported, view, commands));
691
+ }
692
+ return {
693
+ channels,
694
+ warnings: describer.warnings
695
+ };
696
+ }
697
+ /**
698
+ * Every component `file` exports, described: a function whose first
699
+ * parameter is `Inputs<P>`, or a class extending `Component`, whose
700
+ * inputs are its `InputCell` fields. A library's index re-exports its
701
+ * components, and reading it reads all of them.
702
+ */
703
+ readComponents(file, config) {
704
+ const opened = this.open(file, config);
705
+ if (typeof opened === "string") return {
706
+ components: /* @__PURE__ */ new Map(),
707
+ warnings: [opened]
708
+ };
709
+ const { checker, exports } = opened;
710
+ const describer = new Describer(this.ts, checker, "component");
711
+ const components = /* @__PURE__ */ new Map();
712
+ for (const exported of exports) {
713
+ const symbol = exported.flags & this.ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
714
+ const type = checker.getTypeOfSymbol(symbol);
715
+ const inputs = type === void 0 ? void 0 : this.inputsOf(checker, type);
716
+ if (inputs !== void 0) components.set(exported.name, describer.component(symbol, inputs));
717
+ }
718
+ return {
719
+ components,
720
+ warnings: describer.warnings
721
+ };
722
+ }
723
+ /**
724
+ * The source files of the project `config` describes, with their
725
+ * text: what the project itself wrote, so neither its dependencies
726
+ * nor declaration files. The project's own tsconfig decides what that
727
+ * is, which is the answer the person already gave their editor.
728
+ */
729
+ sources(config) {
730
+ const program = this.snapshot({ openProjects: [config] }).getProject(config)?.program;
731
+ if (program === void 0) return [];
732
+ const out = [];
733
+ for (const file of program.getSourceFileNames()) {
734
+ if (file.includes("/node_modules/") || /\.d\.[cm]?ts$/.test(file)) continue;
735
+ const text = program.getSourceFile(file)?.text;
736
+ if (text !== void 0) out.push({
737
+ file,
738
+ text
739
+ });
740
+ }
741
+ return out;
742
+ }
743
+ /** The inputs a component's type declares, or undefined when it is not a component. */
744
+ inputsOf(checker, type) {
745
+ const { Call, Construct } = this.ts.SignatureKind;
746
+ const [call] = checker.getSignaturesOfType(type, Call);
747
+ const first = call?.getParameters()[0];
748
+ if (first !== void 0) {
749
+ const parameter = checker.getTypeOfSymbol(first);
750
+ const returns = checker.getReturnTypeOfSignature(call);
751
+ if (parameter?.getAliasSymbol()?.name === "Inputs" && returns !== void 0 && this.drawable(returns)) {
752
+ const [props] = parameter.getAliasTypeArguments();
753
+ return props === void 0 ? void 0 : {
754
+ kind: "props",
755
+ type: props
756
+ };
757
+ }
758
+ return;
759
+ }
760
+ const [construct] = checker.getSignaturesOfType(type, Construct);
761
+ const instance = construct === void 0 ? void 0 : checker.getReturnTypeOfSignature(construct);
762
+ if (instance === void 0 || !this.extendsComponent(instance)) return;
763
+ const cells = [];
764
+ for (const property of checker.getPropertiesOfType(instance)) {
765
+ const propertyType = checker.getTypeOfSymbol(property);
766
+ if (propertyType?.getSymbol()?.name === "InputCell") {
767
+ const [value] = checker.getTypeArguments(propertyType);
768
+ if (value !== void 0) cells.push({
769
+ symbol: property,
770
+ type: value
771
+ });
772
+ }
773
+ }
774
+ return {
775
+ kind: "cells",
776
+ cells
777
+ };
778
+ }
779
+ /** Whether a function returning `type` returns a child: `UiChild`, an element, or a union of them. */
780
+ drawable(type) {
781
+ if (type.getAliasSymbol()?.name === "UiChild") return true;
782
+ return (type.isUnionType() ? type.getTypes() ?? [] : [type]).some((member) => CHILD.has(member.getSymbol()?.name ?? ""));
783
+ }
784
+ extendsComponent(type) {
785
+ for (let depth = 0, current = type; current !== void 0 && depth < 16; depth++) {
786
+ if (current.getSymbol()?.name === "Component") return true;
787
+ current = current.isClassOrInterface() ? current.getBaseTypes()?.[0] : void 0;
788
+ }
789
+ return false;
790
+ }
791
+ /**
792
+ * The checker and the module's exports, or why there are none: read
793
+ * in the project `preferred` names when it includes the file (the
794
+ * catalog's own tsconfig, which may resolve its imports differently
795
+ * from any beside it), else the project TypeScript finds for it.
796
+ */
797
+ open(file, preferred) {
798
+ this.changed.add(file);
799
+ let snapshot = this.snapshot(preferred === void 0 ? {} : { openProjects: [preferred] });
800
+ let config = preferred !== void 0 && snapshot.getProject(preferred)?.program.getSourceFile(file) !== void 0 ? preferred : this.configs.get(file);
801
+ if (config === void 0) {
802
+ config = this.snapshot({ openFiles: [file] }).getDefaultProjectForFile(file)?.configFileName;
803
+ snapshot = this.snapshot({
804
+ ...config === void 0 ? {} : { openProjects: [config] },
805
+ closeFiles: [file]
806
+ });
807
+ if (config !== void 0) this.configs.set(file, config);
808
+ }
809
+ const project = config === void 0 ? void 0 : snapshot.getProject(config);
810
+ const source = project?.program.getSourceFile(file);
811
+ if (project === void 0 || source === void 0) return `TypeScript has no project that includes ${file}.`;
812
+ const checker = project.checker;
813
+ const module = checker.getSymbolAtLocation(source);
814
+ return {
815
+ checker,
816
+ exports: module === void 0 ? [] : checker.getExportsOfModule(module)
817
+ };
818
+ }
819
+ close() {
820
+ this.api?.close();
821
+ this.api = null;
822
+ }
823
+ };
824
+ /**
825
+ * Library types that are objects to the checker and not plain data to
826
+ * a channel. They compare by reference, so `provide` rejects them on
827
+ * their first value; describing their methods as properties would be
828
+ * describing something that never arrives.
829
+ */
830
+ const NOT_PLAIN = /* @__PURE__ */ new Set([
831
+ "Date",
832
+ "Map",
833
+ "ReadonlyMap",
834
+ "Set",
835
+ "ReadonlySet",
836
+ "WeakMap",
837
+ "WeakSet",
838
+ "Promise",
839
+ "RegExp",
840
+ "Error",
841
+ "ArrayBuffer",
842
+ "SharedArrayBuffer",
843
+ "DataView",
844
+ "Blob",
845
+ "File",
846
+ "URL",
847
+ "Int8Array",
848
+ "Uint8Array",
849
+ "Uint8ClampedArray",
850
+ "Int16Array",
851
+ "Uint16Array",
852
+ "Int32Array",
853
+ "Uint32Array",
854
+ "Float32Array",
855
+ "Float64Array",
856
+ "BigInt64Array",
857
+ "BigUint64Array"
858
+ ]);
859
+ /**
860
+ * Bytes, which a command may carry and a view key may not.
861
+ *
862
+ * A command's argument is structured-cloned onto the owning thread, and
863
+ * a typed array or an `ArrayBuffer` clones as itself: a file a person
864
+ * dropped crosses as its bytes, with no encoding pass on the render
865
+ * thread. A view key is diffed, and a differ that compares bytes by
866
+ * reference reports a change on every emission, so there they stay
867
+ * unplain. In a command's schema they are a base64 string, which is
868
+ * what an agent can write, tagged with the type the command expects so
869
+ * the agent surface can turn the string back into it.
870
+ */
871
+ const BINARY = /* @__PURE__ */ new Set([
872
+ "ArrayBuffer",
873
+ "Int8Array",
874
+ "Uint8Array",
875
+ "Uint8ClampedArray",
876
+ "Int16Array",
877
+ "Uint16Array",
878
+ "Int32Array",
879
+ "Uint32Array",
880
+ "Float32Array",
881
+ "Float64Array",
882
+ "BigInt64Array",
883
+ "BigUint64Array"
884
+ ]);
885
+ /**
886
+ * The types a component takes as a child. `UiChild` is a union of
887
+ * these and an Observable of them, and describing its members would
888
+ * describe the element records a tag compiles to, which nobody writes.
889
+ */
890
+ const CHILD = /* @__PURE__ */ new Set([
891
+ "UiElement",
892
+ "ComponentLikeElement",
893
+ "ComponentElement"
894
+ ]);
895
+ /**
896
+ * Turns checker types into JSON Schema for one module's channels or
897
+ * components.
898
+ *
899
+ * The two differ in what is allowed. A channel's values cross a thread
900
+ * as plain data, so a function or a class instance in one is a mistake,
901
+ * and is reported. A component's inputs stay on the render thread, where
902
+ * a callback is how a component talks back and a child is how it is
903
+ * composed, so there they are described for what they are and nothing
904
+ * is reported.
905
+ */
906
+ var Describer = class {
907
+ warnings = [];
908
+ flags;
909
+ /** Describing a command's parameters, where bytes are allowed; see `BINARY`. */
910
+ inCommand = false;
911
+ /** Types being described right now, to notice a type that contains itself. */
912
+ inProgress = /* @__PURE__ */ new Set();
913
+ /** Types found to contain themselves, which are described once in `$defs`. */
914
+ recursive = /* @__PURE__ */ new Set();
915
+ /** `$defs` for the schema being built. Reset per root, so each schema stands alone. */
916
+ defs = /* @__PURE__ */ new Map();
917
+ defNames = /* @__PURE__ */ new Map();
918
+ /** In a component, each named type's `$defs` entry, or null when it is small enough to repeat. */
919
+ shared = /* @__PURE__ */ new Map();
920
+ ts;
921
+ checker;
922
+ mode;
923
+ constructor(ts, checker, mode = "channel") {
924
+ this.ts = ts;
925
+ this.checker = checker;
926
+ this.flags = ts.TypeFlags;
927
+ this.mode = mode;
928
+ }
929
+ component(symbol, inputs) {
930
+ this.defs = /* @__PURE__ */ new Map();
931
+ this.shared = /* @__PURE__ */ new Map();
932
+ const checker = this.checker;
933
+ const properties = {};
934
+ const required = [];
935
+ const entries = inputs.kind === "cells" ? inputs.cells : checker.getPropertiesOfType(inputs.type).map((property) => ({
936
+ symbol: property,
937
+ type: checker.getTypeOfSymbol(property)
938
+ }));
939
+ const own = inputs.kind === "props" ? (inputs.type.getAliasSymbol() ?? inputs.type.getSymbol())?.name : void 0;
940
+ for (const { symbol: property, type: given } of entries) {
941
+ const path = `${symbol.name}.${property.name}`;
942
+ const type = given !== void 0 && this.includesUndefined(given) && !this.includesNull(given) ? checker.getNonNullableType(given) ?? given : given;
943
+ let schema = type === void 0 ? {} : this.describe(type, path);
944
+ if (type !== void 0) schema = {
945
+ ...schema,
946
+ "x-gesso-type": this.typeText(type)
947
+ };
948
+ const description = checker.getDocumentationCommentOfSymbol(property);
949
+ if (description !== "") schema.description = description;
950
+ const fallback = checker.getJsDocTagsOfSymbol(property).find((tag) => tag.name === "default");
951
+ if (fallback?.text !== void 0) schema.default = parseDefault(fallback.text);
952
+ const from = property.getParent()?.name;
953
+ if (own !== void 0 && from !== void 0 && from !== own && !from.startsWith("__")) schema["x-gesso-from"] = from;
954
+ properties[property.name] = schema;
955
+ if (!(inputs.kind === "cells" || (property.flags & this.ts.SymbolFlags.Optional) !== 0 || given !== void 0 && this.includesUndefined(given))) required.push(property.name);
956
+ }
957
+ const schema = {
958
+ type: "object",
959
+ properties,
960
+ additionalProperties: false
961
+ };
962
+ if (required.length > 0) schema.required = required;
963
+ if (this.defs.size > 0) schema.$defs = Object.fromEntries(this.defs);
964
+ const out = { inputs: schema };
965
+ const description = this.componentDocumentation(symbol, inputs);
966
+ if (description !== "") {
967
+ out.summary = description.split(/\n\s*\n/)[0].replace(/\s+/g, " ").trim();
968
+ out.description = description;
969
+ }
970
+ const tags = checker.getJsDocTagsOfSymbol(symbol);
971
+ const examples = tags.filter((tag) => tag.name === "example" && tag.text !== void 0).map((tag) => tag.text);
972
+ if (examples.length > 0) out.examples = examples;
973
+ const deprecated = tags.find((tag) => tag.name === "deprecated");
974
+ if (deprecated !== void 0) out.deprecated = deprecated.text === void 0 || deprecated.text.trim() === "" ? true : deprecated.text.trim();
975
+ return out;
976
+ }
977
+ /**
978
+ * What a component is for: its own JSDoc, else its props type's. A
979
+ * comment elsewhere in its file is not read: no rule about which one
980
+ * describes the component holds across files, and a summary that is
981
+ * sometimes about something else is worse to an agent than none.
982
+ */
983
+ componentDocumentation(symbol, inputs) {
984
+ const own = this.checker.getDocumentationCommentOfSymbol(symbol);
985
+ if (own !== "") return own;
986
+ const props = inputs.kind === "props" ? inputs.type.getAliasSymbol() ?? inputs.type.getSymbol() : void 0;
987
+ return props === void 0 ? "" : this.checker.getDocumentationCommentOfSymbol(props);
988
+ }
989
+ /** The type as TypeScript writes it, without the `undefined` an optional input carries. */
990
+ typeText(type) {
991
+ const defined = this.includesUndefined(type) ? this.checker.getNonNullableType(type) : type;
992
+ const text = this.checker.typeToString(defined ?? type);
993
+ return this.includesNull(type) && !/\bnull\b/.test(text) ? `${text} | null` : text;
994
+ }
995
+ includesNull(type) {
996
+ const isNull = (member) => (member.flags & this.flags.Null) !== 0;
997
+ return type.isUnionType() ? (type.getTypes() ?? []).some(isNull) : isNull(type);
998
+ }
999
+ channel(token, view, commands) {
1000
+ const out = {
1001
+ view: this.root(view, `${token.name}.view`),
1002
+ commands: {}
1003
+ };
1004
+ const description = this.checker.getDocumentationCommentOfSymbol(token);
1005
+ if (description !== "") out.description = description;
1006
+ if (this.checker.getJsDocTagsOfSymbol(token).some((tag) => tag.name === "hidden")) out.internal = true;
1007
+ const hidden = this.checker.getPropertiesOfType(view).filter((key) => this.checker.getJsDocTagsOfSymbol(key).some((tag) => tag.name === "hidden")).map((key) => key.name);
1008
+ if (hidden.length > 0) out.hidden = hidden;
1009
+ if (commands !== void 0) for (const command of this.checker.getPropertiesOfType(commands)) {
1010
+ const described = this.command(command, `${token.name}.commands.${command.name}`);
1011
+ if (described !== null) out.commands[command.name] = described;
1012
+ }
1013
+ return out;
1014
+ }
1015
+ /** A schema that stands alone: its own `$defs`, if anything in it recurs. */
1016
+ root(type, path) {
1017
+ this.defs = /* @__PURE__ */ new Map();
1018
+ const schema = this.describe(type, path);
1019
+ if (this.defs.size > 0) return {
1020
+ ...schema,
1021
+ $defs: Object.fromEntries(this.defs)
1022
+ };
1023
+ return schema;
1024
+ }
1025
+ command(symbol, path) {
1026
+ const type = this.checker.getTypeOfSymbol(symbol);
1027
+ const signatures = type === void 0 ? [] : this.checker.getSignaturesOfType(type, this.ts.SignatureKind.Call);
1028
+ if (signatures.length === 0) {
1029
+ this.warnings.push(`${path} is not a function, so it is not a command an agent can send.`);
1030
+ return null;
1031
+ }
1032
+ if (signatures.length > 1) this.warnings.push(`${path} is overloaded; only its first signature is described.`);
1033
+ const signature = signatures[0];
1034
+ const tags = this.checker.getJsDocTagsOfSymbol(symbol);
1035
+ const parameters = signature.getParameters();
1036
+ this.defs = /* @__PURE__ */ new Map();
1037
+ const properties = {};
1038
+ const required = [];
1039
+ this.inCommand = true;
1040
+ parameters.forEach((parameter, index) => {
1041
+ const parameterType = this.checker.getTypeOfSymbol(parameter);
1042
+ const optional = parameterType !== void 0 && this.includesUndefined(parameterType);
1043
+ const schema = parameterType === void 0 ? {} : this.describe(parameterType, `${path}(${parameter.name})`);
1044
+ const description = this.parameterDescription(parameter, tags);
1045
+ properties[parameter.name] = description === "" ? schema : {
1046
+ ...schema,
1047
+ description
1048
+ };
1049
+ const rest = signature.hasRestParameter && index === parameters.length - 1;
1050
+ if (!optional && !rest) required.push(parameter.name);
1051
+ });
1052
+ this.inCommand = false;
1053
+ const input = {
1054
+ type: "object",
1055
+ properties,
1056
+ additionalProperties: false
1057
+ };
1058
+ if (required.length > 0) input.required = required;
1059
+ if (this.defs.size > 0) input.$defs = Object.fromEntries(this.defs);
1060
+ const out = {
1061
+ parameters: parameters.map((parameter) => parameter.name),
1062
+ input
1063
+ };
1064
+ const description = this.checker.getDocumentationCommentOfSymbol(symbol);
1065
+ if (description !== "") out.description = description;
1066
+ if (signature.hasRestParameter) out.rest = true;
1067
+ for (const annotation of [
1068
+ "destructive",
1069
+ "idempotent",
1070
+ "confirm",
1071
+ "hidden"
1072
+ ]) if (tags.some((tag) => tag.name === annotation)) out[annotation] = true;
1073
+ return out;
1074
+ }
1075
+ /**
1076
+ * A parameter's own comment, or failing that its `@param` line.
1077
+ *
1078
+ * The checker attaches `@param` text to the parameter of a method
1079
+ * signature. A command written as a property, `move: (from: number)
1080
+ * => void`, keeps its comment on the property, so the tag is read
1081
+ * from there instead.
1082
+ */
1083
+ parameterDescription(parameter, tags) {
1084
+ const own = this.checker.getDocumentationCommentOfSymbol(parameter);
1085
+ if (own !== "") return own;
1086
+ for (const tag of tags) {
1087
+ const match = tag.name === "param" ? /^\s*(\S+)\s*(?:-\s*)?([\s\S]*)$/.exec(tag.text ?? "") : null;
1088
+ if (match !== null && match[1] === parameter.name) return match[2].trim();
1089
+ }
1090
+ return "";
1091
+ }
1092
+ includesUndefined(type) {
1093
+ const { Undefined, Void } = this.flags;
1094
+ if (type.isUnionType()) return (type.getTypes() ?? []).some((member) => (member.flags & (Undefined | Void)) !== 0);
1095
+ return (type.flags & (Undefined | Void)) !== 0;
1096
+ }
1097
+ /**
1098
+ * In a component, a named type an input uses (`UiLength`, a props
1099
+ * interface) is described once, in `$defs`, and referred to after.
1100
+ * Every control takes the same layout inputs, and spelled out at each
1101
+ * use they were most of each component's description: an agent reads
1102
+ * these, and pays for every repetition.
1103
+ */
1104
+ describe(type, path) {
1105
+ if (this.mode !== "component") return this.describeType(type, path);
1106
+ const known = this.shared.get(type.id);
1107
+ if (known !== void 0) return known === null ? this.describeType(type, path) : { $ref: `#/$defs/${known}` };
1108
+ const name = (type.getAliasSymbol() ?? type.getSymbol())?.name;
1109
+ if (name === void 0 || name.startsWith("__") || name === "Array" || name === "ReadonlyArray") return this.describeType(type, path);
1110
+ const schema = this.describeType(type, path);
1111
+ if (schema.$ref !== void 0 || isSmall(schema)) {
1112
+ this.shared.set(type.id, null);
1113
+ return schema;
1114
+ }
1115
+ const defName = this.defName(type);
1116
+ this.defs.set(defName, schema);
1117
+ this.shared.set(type.id, defName);
1118
+ return { $ref: `#/$defs/${defName}` };
1119
+ }
1120
+ describeType(type, path) {
1121
+ const f = this.flags;
1122
+ const flags = type.flags;
1123
+ if (flags & (f.Any | f.Unknown)) return {};
1124
+ if (flags & f.Never) {
1125
+ this.warnings.push(`${path} is never, which describes no value. An empty array literal is never[] unless it says what it holds: write \`[] as Row[]\`.`);
1126
+ return { not: {} };
1127
+ }
1128
+ if (flags & (f.String | f.TemplateLiteral | f.StringMapping)) return { type: "string" };
1129
+ if (flags & f.Number) return { type: "number" };
1130
+ if (flags & f.Boolean) return { type: "boolean" };
1131
+ if (flags & f.Null) return { type: "null" };
1132
+ if (flags & (f.Undefined | f.Void)) return {};
1133
+ if (flags & f.BooleanLiteral) return { const: type.intrinsicName === "true" };
1134
+ if (flags & (f.StringLiteral | f.NumberLiteral)) return { const: type.value };
1135
+ if (flags & (f.BigInt | f.BigIntLiteral | f.ESSymbol | f.UniqueESSymbol)) return this.unplain(path, flags & f.BigIntLike ? "a bigint" : "a symbol");
1136
+ if (type.isUnionType()) return this.union(type, path);
1137
+ if (flags & f.TypeParameter) return {};
1138
+ if (flags & (f.Object | f.Intersection)) {
1139
+ if (this.checker.isArrayType(type)) {
1140
+ const [element] = this.checker.getTypeArguments(type);
1141
+ return {
1142
+ type: "array",
1143
+ items: element === void 0 ? {} : this.describe(element, `${path}[]`)
1144
+ };
1145
+ }
1146
+ if (this.checker.isTupleType(type)) return this.tuple(type, path);
1147
+ return this.named(type, path);
1148
+ }
1149
+ this.warnings.push(`${path} has a type this reader does not describe, so it is left open.`);
1150
+ return {};
1151
+ }
1152
+ /**
1153
+ * A union, with `undefined` taken out (optionality is said by
1154
+ * `required`), `true | false` folded back into `boolean`, and a union
1155
+ * of nothing but literals written as an `enum`.
1156
+ */
1157
+ union(type, path) {
1158
+ const f = this.flags;
1159
+ let members = (type.getTypes() ?? []).filter((member) => (member.flags & (f.Undefined | f.Void)) === 0);
1160
+ if (this.mode === "component" && members.some((member) => CHILD.has(member.getSymbol()?.name ?? ""))) return { "x-gesso-child": true };
1161
+ const booleans = members.filter((member) => member.flags & f.BooleanLiteral);
1162
+ const schemas = [];
1163
+ if (booleans.length === 2) {
1164
+ members = members.filter((member) => !(member.flags & f.BooleanLiteral));
1165
+ schemas.push({ type: "boolean" });
1166
+ }
1167
+ if (members.length > 0 && schemas.length === 0 && members.every((member) => member.flags & (f.StringLiteral | f.NumberLiteral))) return { enum: members.map((member) => member.value) };
1168
+ members.sort((a, b) => Number((a.flags & f.Null) !== 0) - Number((b.flags & f.Null) !== 0));
1169
+ schemas.push(...members.map((member) => this.describe(member, path)));
1170
+ return schemas.length === 1 ? schemas[0] : { anyOf: schemas };
1171
+ }
1172
+ /**
1173
+ * An object type, described once however often it appears, and by
1174
+ * reference when it contains itself.
1175
+ */
1176
+ named(type, path) {
1177
+ const id = type.id;
1178
+ if (this.inProgress.has(id)) {
1179
+ this.recursive.add(id);
1180
+ return { $ref: `#/$defs/${this.defName(type)}` };
1181
+ }
1182
+ this.inProgress.add(id);
1183
+ const schema = this.object(type, path);
1184
+ this.inProgress.delete(id);
1185
+ if (this.recursive.has(id)) {
1186
+ const name = this.defName(type);
1187
+ this.defs.set(name, schema);
1188
+ return { $ref: `#/$defs/${name}` };
1189
+ }
1190
+ return schema;
1191
+ }
1192
+ defName(type) {
1193
+ let name = this.defNames.get(type.id);
1194
+ if (name === void 0) {
1195
+ const base = (type.getAliasSymbol() ?? type.getSymbol())?.name ?? "Type";
1196
+ name = new Set(this.defNames.values()).has(base) ? `${base}${type.id}` : base;
1197
+ this.defNames.set(type.id, name);
1198
+ }
1199
+ return name;
1200
+ }
1201
+ object(type, path) {
1202
+ const checker = this.checker;
1203
+ const symbolName = type.getSymbol()?.name;
1204
+ if (this.mode === "component") {
1205
+ if (symbolName !== void 0 && CHILD.has(symbolName)) return { "x-gesso-child": true };
1206
+ if (checker.getSignaturesOfType(type, this.ts.SignatureKind.Call).length > 0) return { "x-gesso-callback": true };
1207
+ if (symbolName !== void 0 && NOT_PLAIN.has(symbolName) || ((type.getSymbol()?.flags ?? 0) & this.ts.SymbolFlags.Class) !== 0) return { "x-gesso-type": checker.typeToString(type) };
1208
+ }
1209
+ if (symbolName !== void 0 && this.inCommand && BINARY.has(symbolName)) return {
1210
+ type: "string",
1211
+ contentEncoding: "base64",
1212
+ "x-gesso-binary": symbolName
1213
+ };
1214
+ if (symbolName === "SharedArrayBuffer" && !this.inCommand) return {
1215
+ type: "string",
1216
+ description: "Shared memory, shown as its size.",
1217
+ "x-gesso-shared": true
1218
+ };
1219
+ if (symbolName !== void 0 && NOT_PLAIN.has(symbolName)) return this.unplain(path, `a ${symbolName}`);
1220
+ if (type.objectFlags & this.ts.ObjectFlags.Class) return this.unplain(path, `an instance of ${symbolName ?? "a class"}`);
1221
+ if (checker.getSignaturesOfType(type, this.ts.SignatureKind.Call).length > 0) return this.unplain(path, "a function");
1222
+ const properties = {};
1223
+ const required = [];
1224
+ for (const property of checker.getPropertiesOfType(type)) {
1225
+ const propertyType = checker.getTypeOfSymbol(property);
1226
+ const at = `${path}.${property.name}`;
1227
+ if (propertyType === void 0) {
1228
+ properties[property.name] = {};
1229
+ continue;
1230
+ }
1231
+ const schema = this.describe(propertyType, at);
1232
+ const description = checker.getDocumentationCommentOfSymbol(property);
1233
+ properties[property.name] = description === "" ? schema : {
1234
+ ...schema,
1235
+ description
1236
+ };
1237
+ if (!((property.flags & this.ts.SymbolFlags.Optional) !== 0 || this.includesUndefined(propertyType))) required.push(property.name);
1238
+ }
1239
+ const schema = {
1240
+ type: "object",
1241
+ properties
1242
+ };
1243
+ if (required.length > 0) schema.required = required;
1244
+ const index = checker.getIndexInfosOfType(type).find((info) => info.keyType.flags & this.flags.String);
1245
+ if (index !== void 0) schema.additionalProperties = this.describe(index.valueType, `${path}[key]`);
1246
+ return schema;
1247
+ }
1248
+ tuple(type, path) {
1249
+ const elements = this.checker.getTypeArguments(type);
1250
+ const elementFlags = type.getTarget().elementFlags ?? [];
1251
+ const { Required, Rest } = this.ts.ElementFlags;
1252
+ const prefixItems = [];
1253
+ let items = false;
1254
+ let minItems = 0;
1255
+ elements.forEach((element, index) => {
1256
+ const schema = this.describe(element, `${path}[${index}]`);
1257
+ if (elementFlags[index] & Rest) items = schema;
1258
+ else {
1259
+ prefixItems.push(schema);
1260
+ if (elementFlags[index] & Required) minItems = index + 1;
1261
+ }
1262
+ });
1263
+ return {
1264
+ type: "array",
1265
+ prefixItems,
1266
+ items,
1267
+ minItems
1268
+ };
1269
+ }
1270
+ unplain(path, what) {
1271
+ if (this.mode === "component") return {};
1272
+ this.warnings.push(`${path} is ${what}, which is not plain data and cannot cross a channel.`);
1273
+ return {};
1274
+ }
1275
+ };
1276
+ /**
1277
+ * A `@default` tag's value: JSON when it is JSON (`99`, `'neutral'`
1278
+ * written with double quotes, `true`), a single-quoted string unquoted,
1279
+ * and otherwise the text as written.
1280
+ */
1281
+ function parseDefault(text) {
1282
+ const trimmed = text.trim();
1283
+ try {
1284
+ return JSON.parse(trimmed);
1285
+ } catch {
1286
+ const quoted = /^'(.*)'$/.exec(trimmed) ?? /^`(.*)`$/.exec(trimmed);
1287
+ return quoted === null ? trimmed : quoted[1];
1288
+ }
1289
+ }
1290
+ /** Whether a schema is as short as a reference to it would be. */
1291
+ function isSmall(schema) {
1292
+ if (schema.enum !== void 0 || schema.const !== void 0) return true;
1293
+ const keys = Object.keys(schema);
1294
+ return keys.length === 0 || keys.length === 1 && (typeof schema.type === "string" || keys[0].startsWith("x-gesso-"));
1295
+ }
1296
+ //#endregion
1297
+ export { blankLiterals as a, firstArgumentName as c, CATALOG_TOOLS as d, ComponentCatalog as f, describedModule as i, importSources as l, declaresChannel as n, findCall as o, mayDeclareComponent as p, describeCalls as r, findCalls as s, ContractReader as t, CATALOG_INSTRUCTIONS as u };
1298
+
1299
+ //# sourceMappingURL=contracts-Dwn5pg-0.js.map