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.
- package/CHANGELOG.md +28 -0
- package/dist/catalog-CbeOrdxD.d.ts +305 -0
- package/dist/contracts-Dwn5pg-0.js +1299 -0
- package/dist/contracts-Dwn5pg-0.js.map +1 -0
- package/dist/gesso-channels.mjs +322 -24
- package/dist/gesso-channels.mjs.map +1 -1
- package/dist/index.d.ts +42 -114
- package/dist/index.js +317 -710
- package/dist/index.js.map +1 -1
- package/dist/library.d.ts +56 -0
- package/dist/library.js +56 -0
- package/dist/library.js.map +1 -0
- package/package.json +5 -1
|
@@ -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
|