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.
@@ -52,6 +52,8 @@ function importPath(fromDir, file) {
52
52
  var ContractReader = class {
53
53
  api = null;
54
54
  changed = /* @__PURE__ */ new Set();
55
+ created = /* @__PURE__ */ new Set();
56
+ deleted = /* @__PURE__ */ new Set();
55
57
  /** Each contract's tsconfig, found once. */
56
58
  configs = /* @__PURE__ */ new Map();
57
59
  /**
@@ -66,39 +68,69 @@ var ContractReader = class {
66
68
  this.cwd = cwd;
67
69
  this.fs = fs;
68
70
  }
69
- /** A file changed on disk since the last read. */
70
- invalidate(file) {
71
- this.changed.add(file);
71
+ /**
72
+ * A file changed on disk since the last read. A new or deleted one is
73
+ * said so, because a program's list of files only changes for those:
74
+ * a component in a new file is otherwise not in the catalog.
75
+ */
76
+ invalidate(file, event = "update") {
77
+ (event === "create" ? this.created : event === "delete" ? this.deleted : this.changed).add(file);
72
78
  }
73
- /** Every channel `file` exports, described. */
74
- read(file) {
79
+ /**
80
+ * A snapshot of the projects, with what changed since the last one.
81
+ *
82
+ * The checker keeps every project it has opened, and asked to update
83
+ * one whose tsconfig has since been deleted it refuses the whole
84
+ * update ("project not found for update"). That was a dev server gone
85
+ * with it: a project's tsconfig removed while the server ran threw out
86
+ * of the catalog's read and took the process down. A refused update is
87
+ * a checker started afresh, which reads the disk as it now is and
88
+ * opens only what is asked of it again.
89
+ */
90
+ snapshot(request) {
75
91
  const api = this.api ??= new this.ts.API({
76
92
  cwd: this.cwd,
77
93
  ...this.fs === void 0 ? {} : { fs: this.fs }
78
94
  });
79
- this.changed.add(file);
80
- let snapshot = api.updateSnapshot({ fileChanges: { changed: [...this.changed] } });
81
- this.changed.clear();
82
- let config = this.configs.get(file);
83
- if (config === void 0) {
84
- config = api.updateSnapshot({ openFiles: [file] }).getDefaultProjectForFile(file)?.configFileName;
85
- snapshot = api.updateSnapshot({
86
- ...config === void 0 ? {} : { openProjects: [config] },
87
- closeFiles: [file]
95
+ try {
96
+ return api.updateSnapshot({
97
+ ...request,
98
+ fileChanges: this.takeChanges()
88
99
  });
89
- if (config !== void 0) this.configs.set(file, config);
100
+ } catch {
101
+ this.close();
102
+ this.configs.clear();
103
+ const fresh = this.api = new this.ts.API({
104
+ cwd: this.cwd,
105
+ ...this.fs === void 0 ? {} : { fs: this.fs }
106
+ });
107
+ const { fileChanges: _changes, ...rest } = request;
108
+ return fresh.updateSnapshot(rest);
90
109
  }
91
- const project = config === void 0 ? void 0 : snapshot.getProject(config);
92
- const source = project?.program.getSourceFile(file);
93
- if (project === void 0 || source === void 0) return {
110
+ }
111
+ /** What changed since the last snapshot, emptied for the next. */
112
+ takeChanges() {
113
+ const changes = {
114
+ changed: [...this.changed],
115
+ created: [...this.created],
116
+ deleted: [...this.deleted]
117
+ };
118
+ this.changed.clear();
119
+ this.created.clear();
120
+ this.deleted.clear();
121
+ return changes;
122
+ }
123
+ /** Every channel `file` exports, described. */
124
+ read(file) {
125
+ const opened = this.open(file);
126
+ if (typeof opened === "string") return {
94
127
  channels: /* @__PURE__ */ new Map(),
95
- warnings: [`TypeScript has no project that includes ${file}.`]
128
+ warnings: [opened]
96
129
  };
97
- const checker = project.checker;
98
- const module = checker.getSymbolAtLocation(source);
130
+ const { checker, exports } = opened;
99
131
  const describer = new Describer(this.ts, checker);
100
132
  const channels = /* @__PURE__ */ new Map();
101
- for (const exported of module === void 0 ? [] : checker.getExportsOfModule(module)) {
133
+ for (const exported of exports) {
102
134
  const type = checker.getTypeOfSymbol(exported);
103
135
  if (type === void 0 || type.getSymbol()?.name !== "ChannelToken") continue;
104
136
  const [view, commands] = checker.getTypeArguments(type);
@@ -109,6 +141,128 @@ var ContractReader = class {
109
141
  warnings: describer.warnings
110
142
  };
111
143
  }
144
+ /**
145
+ * Every component `file` exports, described: a function whose first
146
+ * parameter is `Inputs<P>`, or a class extending `Component`, whose
147
+ * inputs are its `InputCell` fields. A library's index re-exports its
148
+ * components, and reading it reads all of them.
149
+ */
150
+ readComponents(file, config) {
151
+ const opened = this.open(file, config);
152
+ if (typeof opened === "string") return {
153
+ components: /* @__PURE__ */ new Map(),
154
+ warnings: [opened]
155
+ };
156
+ const { checker, exports } = opened;
157
+ const describer = new Describer(this.ts, checker, "component");
158
+ const components = /* @__PURE__ */ new Map();
159
+ for (const exported of exports) {
160
+ const symbol = exported.flags & this.ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
161
+ const type = checker.getTypeOfSymbol(symbol);
162
+ const inputs = type === void 0 ? void 0 : this.inputsOf(checker, type);
163
+ if (inputs !== void 0) components.set(exported.name, describer.component(symbol, inputs));
164
+ }
165
+ return {
166
+ components,
167
+ warnings: describer.warnings
168
+ };
169
+ }
170
+ /**
171
+ * The source files of the project `config` describes, with their
172
+ * text: what the project itself wrote, so neither its dependencies
173
+ * nor declaration files. The project's own tsconfig decides what that
174
+ * is, which is the answer the person already gave their editor.
175
+ */
176
+ sources(config) {
177
+ const program = this.snapshot({ openProjects: [config] }).getProject(config)?.program;
178
+ if (program === void 0) return [];
179
+ const out = [];
180
+ for (const file of program.getSourceFileNames()) {
181
+ if (file.includes("/node_modules/") || /\.d\.[cm]?ts$/.test(file)) continue;
182
+ const text = program.getSourceFile(file)?.text;
183
+ if (text !== void 0) out.push({
184
+ file,
185
+ text
186
+ });
187
+ }
188
+ return out;
189
+ }
190
+ /** The inputs a component's type declares, or undefined when it is not a component. */
191
+ inputsOf(checker, type) {
192
+ const { Call, Construct } = this.ts.SignatureKind;
193
+ const [call] = checker.getSignaturesOfType(type, Call);
194
+ const first = call?.getParameters()[0];
195
+ if (first !== void 0) {
196
+ const parameter = checker.getTypeOfSymbol(first);
197
+ const returns = checker.getReturnTypeOfSignature(call);
198
+ if (parameter?.getAliasSymbol()?.name === "Inputs" && returns !== void 0 && this.drawable(returns)) {
199
+ const [props] = parameter.getAliasTypeArguments();
200
+ return props === void 0 ? void 0 : {
201
+ kind: "props",
202
+ type: props
203
+ };
204
+ }
205
+ return;
206
+ }
207
+ const [construct] = checker.getSignaturesOfType(type, Construct);
208
+ const instance = construct === void 0 ? void 0 : checker.getReturnTypeOfSignature(construct);
209
+ if (instance === void 0 || !this.extendsComponent(instance)) return;
210
+ const cells = [];
211
+ for (const property of checker.getPropertiesOfType(instance)) {
212
+ const propertyType = checker.getTypeOfSymbol(property);
213
+ if (propertyType?.getSymbol()?.name === "InputCell") {
214
+ const [value] = checker.getTypeArguments(propertyType);
215
+ if (value !== void 0) cells.push({
216
+ symbol: property,
217
+ type: value
218
+ });
219
+ }
220
+ }
221
+ return {
222
+ kind: "cells",
223
+ cells
224
+ };
225
+ }
226
+ /** Whether a function returning `type` returns a child: `UiChild`, an element, or a union of them. */
227
+ drawable(type) {
228
+ if (type.getAliasSymbol()?.name === "UiChild") return true;
229
+ return (type.isUnionType() ? type.getTypes() ?? [] : [type]).some((member) => CHILD.has(member.getSymbol()?.name ?? ""));
230
+ }
231
+ extendsComponent(type) {
232
+ for (let depth = 0, current = type; current !== void 0 && depth < 16; depth++) {
233
+ if (current.getSymbol()?.name === "Component") return true;
234
+ current = current.isClassOrInterface() ? current.getBaseTypes()?.[0] : void 0;
235
+ }
236
+ return false;
237
+ }
238
+ /**
239
+ * The checker and the module's exports, or why there are none: read
240
+ * in the project `preferred` names when it includes the file (the
241
+ * catalog's own tsconfig, which may resolve its imports differently
242
+ * from any beside it), else the project TypeScript finds for it.
243
+ */
244
+ open(file, preferred) {
245
+ this.changed.add(file);
246
+ let snapshot = this.snapshot(preferred === void 0 ? {} : { openProjects: [preferred] });
247
+ let config = preferred !== void 0 && snapshot.getProject(preferred)?.program.getSourceFile(file) !== void 0 ? preferred : this.configs.get(file);
248
+ if (config === void 0) {
249
+ config = this.snapshot({ openFiles: [file] }).getDefaultProjectForFile(file)?.configFileName;
250
+ snapshot = this.snapshot({
251
+ ...config === void 0 ? {} : { openProjects: [config] },
252
+ closeFiles: [file]
253
+ });
254
+ if (config !== void 0) this.configs.set(file, config);
255
+ }
256
+ const project = config === void 0 ? void 0 : snapshot.getProject(config);
257
+ const source = project?.program.getSourceFile(file);
258
+ if (project === void 0 || source === void 0) return `TypeScript has no project that includes ${file}.`;
259
+ const checker = project.checker;
260
+ const module = checker.getSymbolAtLocation(source);
261
+ return {
262
+ checker,
263
+ exports: module === void 0 ? [] : checker.getExportsOfModule(module)
264
+ };
265
+ }
112
266
  close() {
113
267
  this.api?.close();
114
268
  this.api = null;
@@ -175,7 +329,27 @@ const BINARY = /* @__PURE__ */ new Set([
175
329
  "BigInt64Array",
176
330
  "BigUint64Array"
177
331
  ]);
178
- /** Turns checker types into JSON Schema for one module's channels. */
332
+ /**
333
+ * The types a component takes as a child. `UiChild` is a union of
334
+ * these and an Observable of them, and describing its members would
335
+ * describe the element records a tag compiles to, which nobody writes.
336
+ */
337
+ const CHILD = /* @__PURE__ */ new Set([
338
+ "UiElement",
339
+ "ComponentLikeElement",
340
+ "ComponentElement"
341
+ ]);
342
+ /**
343
+ * Turns checker types into JSON Schema for one module's channels or
344
+ * components.
345
+ *
346
+ * The two differ in what is allowed. A channel's values cross a thread
347
+ * as plain data, so a function or a class instance in one is a mistake,
348
+ * and is reported. A component's inputs stay on the render thread, where
349
+ * a callback is how a component talks back and a child is how it is
350
+ * composed, so there they are described for what they are and nothing
351
+ * is reported.
352
+ */
179
353
  var Describer = class {
180
354
  warnings = [];
181
355
  flags;
@@ -188,12 +362,86 @@ var Describer = class {
188
362
  /** `$defs` for the schema being built. Reset per root, so each schema stands alone. */
189
363
  defs = /* @__PURE__ */ new Map();
190
364
  defNames = /* @__PURE__ */ new Map();
365
+ /** In a component, each named type's `$defs` entry, or null when it is small enough to repeat. */
366
+ shared = /* @__PURE__ */ new Map();
191
367
  ts;
192
368
  checker;
193
- constructor(ts, checker) {
369
+ mode;
370
+ constructor(ts, checker, mode = "channel") {
194
371
  this.ts = ts;
195
372
  this.checker = checker;
196
373
  this.flags = ts.TypeFlags;
374
+ this.mode = mode;
375
+ }
376
+ component(symbol, inputs) {
377
+ this.defs = /* @__PURE__ */ new Map();
378
+ this.shared = /* @__PURE__ */ new Map();
379
+ const checker = this.checker;
380
+ const properties = {};
381
+ const required = [];
382
+ const entries = inputs.kind === "cells" ? inputs.cells : checker.getPropertiesOfType(inputs.type).map((property) => ({
383
+ symbol: property,
384
+ type: checker.getTypeOfSymbol(property)
385
+ }));
386
+ const own = inputs.kind === "props" ? (inputs.type.getAliasSymbol() ?? inputs.type.getSymbol())?.name : void 0;
387
+ for (const { symbol: property, type: given } of entries) {
388
+ const path = `${symbol.name}.${property.name}`;
389
+ const type = given !== void 0 && this.includesUndefined(given) && !this.includesNull(given) ? checker.getNonNullableType(given) ?? given : given;
390
+ let schema = type === void 0 ? {} : this.describe(type, path);
391
+ if (type !== void 0) schema = {
392
+ ...schema,
393
+ "x-gesso-type": this.typeText(type)
394
+ };
395
+ const description = checker.getDocumentationCommentOfSymbol(property);
396
+ if (description !== "") schema.description = description;
397
+ const fallback = checker.getJsDocTagsOfSymbol(property).find((tag) => tag.name === "default");
398
+ if (fallback?.text !== void 0) schema.default = parseDefault(fallback.text);
399
+ const from = property.getParent()?.name;
400
+ if (own !== void 0 && from !== void 0 && from !== own && !from.startsWith("__")) schema["x-gesso-from"] = from;
401
+ properties[property.name] = schema;
402
+ if (!(inputs.kind === "cells" || (property.flags & this.ts.SymbolFlags.Optional) !== 0 || given !== void 0 && this.includesUndefined(given))) required.push(property.name);
403
+ }
404
+ const schema = {
405
+ type: "object",
406
+ properties,
407
+ additionalProperties: false
408
+ };
409
+ if (required.length > 0) schema.required = required;
410
+ if (this.defs.size > 0) schema.$defs = Object.fromEntries(this.defs);
411
+ const out = { inputs: schema };
412
+ const description = this.componentDocumentation(symbol, inputs);
413
+ if (description !== "") {
414
+ out.summary = description.split(/\n\s*\n/)[0].replace(/\s+/g, " ").trim();
415
+ out.description = description;
416
+ }
417
+ const tags = checker.getJsDocTagsOfSymbol(symbol);
418
+ const examples = tags.filter((tag) => tag.name === "example" && tag.text !== void 0).map((tag) => tag.text);
419
+ if (examples.length > 0) out.examples = examples;
420
+ const deprecated = tags.find((tag) => tag.name === "deprecated");
421
+ if (deprecated !== void 0) out.deprecated = deprecated.text === void 0 || deprecated.text.trim() === "" ? true : deprecated.text.trim();
422
+ return out;
423
+ }
424
+ /**
425
+ * What a component is for: its own JSDoc, else its props type's. A
426
+ * comment elsewhere in its file is not read: no rule about which one
427
+ * describes the component holds across files, and a summary that is
428
+ * sometimes about something else is worse to an agent than none.
429
+ */
430
+ componentDocumentation(symbol, inputs) {
431
+ const own = this.checker.getDocumentationCommentOfSymbol(symbol);
432
+ if (own !== "") return own;
433
+ const props = inputs.kind === "props" ? inputs.type.getAliasSymbol() ?? inputs.type.getSymbol() : void 0;
434
+ return props === void 0 ? "" : this.checker.getDocumentationCommentOfSymbol(props);
435
+ }
436
+ /** The type as TypeScript writes it, without the `undefined` an optional input carries. */
437
+ typeText(type) {
438
+ const defined = this.includesUndefined(type) ? this.checker.getNonNullableType(type) : type;
439
+ const text = this.checker.typeToString(defined ?? type);
440
+ return this.includesNull(type) && !/\bnull\b/.test(text) ? `${text} | null` : text;
441
+ }
442
+ includesNull(type) {
443
+ const isNull = (member) => (member.flags & this.flags.Null) !== 0;
444
+ return type.isUnionType() ? (type.getTypes() ?? []).some(isNull) : isNull(type);
197
445
  }
198
446
  channel(token, view, commands) {
199
447
  const out = {
@@ -293,7 +541,30 @@ var Describer = class {
293
541
  if (type.isUnionType()) return (type.getTypes() ?? []).some((member) => (member.flags & (Undefined | Void)) !== 0);
294
542
  return (type.flags & (Undefined | Void)) !== 0;
295
543
  }
544
+ /**
545
+ * In a component, a named type an input uses (`UiLength`, a props
546
+ * interface) is described once, in `$defs`, and referred to after.
547
+ * Every control takes the same layout inputs, and spelled out at each
548
+ * use they were most of each component's description: an agent reads
549
+ * these, and pays for every repetition.
550
+ */
296
551
  describe(type, path) {
552
+ if (this.mode !== "component") return this.describeType(type, path);
553
+ const known = this.shared.get(type.id);
554
+ if (known !== void 0) return known === null ? this.describeType(type, path) : { $ref: `#/$defs/${known}` };
555
+ const name = (type.getAliasSymbol() ?? type.getSymbol())?.name;
556
+ if (name === void 0 || name.startsWith("__") || name === "Array" || name === "ReadonlyArray") return this.describeType(type, path);
557
+ const schema = this.describeType(type, path);
558
+ if (schema.$ref !== void 0 || isSmall(schema)) {
559
+ this.shared.set(type.id, null);
560
+ return schema;
561
+ }
562
+ const defName = this.defName(type);
563
+ this.defs.set(defName, schema);
564
+ this.shared.set(type.id, defName);
565
+ return { $ref: `#/$defs/${defName}` };
566
+ }
567
+ describeType(type, path) {
297
568
  const f = this.flags;
298
569
  const flags = type.flags;
299
570
  if (flags & (f.Any | f.Unknown)) return {};
@@ -333,6 +604,7 @@ var Describer = class {
333
604
  union(type, path) {
334
605
  const f = this.flags;
335
606
  let members = (type.getTypes() ?? []).filter((member) => (member.flags & (f.Undefined | f.Void)) === 0);
607
+ if (this.mode === "component" && members.some((member) => CHILD.has(member.getSymbol()?.name ?? ""))) return { "x-gesso-child": true };
336
608
  const booleans = members.filter((member) => member.flags & f.BooleanLiteral);
337
609
  const schemas = [];
338
610
  if (booleans.length === 2) {
@@ -376,6 +648,11 @@ var Describer = class {
376
648
  object(type, path) {
377
649
  const checker = this.checker;
378
650
  const symbolName = type.getSymbol()?.name;
651
+ if (this.mode === "component") {
652
+ if (symbolName !== void 0 && CHILD.has(symbolName)) return { "x-gesso-child": true };
653
+ if (checker.getSignaturesOfType(type, this.ts.SignatureKind.Call).length > 0) return { "x-gesso-callback": true };
654
+ if (symbolName !== void 0 && NOT_PLAIN.has(symbolName) || ((type.getSymbol()?.flags ?? 0) & this.ts.SymbolFlags.Class) !== 0) return { "x-gesso-type": checker.typeToString(type) };
655
+ }
379
656
  if (symbolName !== void 0 && this.inCommand && BINARY.has(symbolName)) return {
380
657
  type: "string",
381
658
  contentEncoding: "base64",
@@ -438,10 +715,31 @@ var Describer = class {
438
715
  };
439
716
  }
440
717
  unplain(path, what) {
718
+ if (this.mode === "component") return {};
441
719
  this.warnings.push(`${path} is ${what}, which is not plain data and cannot cross a channel.`);
442
720
  return {};
443
721
  }
444
722
  };
723
+ /**
724
+ * A `@default` tag's value: JSON when it is JSON (`99`, `'neutral'`
725
+ * written with double quotes, `true`), a single-quoted string unquoted,
726
+ * and otherwise the text as written.
727
+ */
728
+ function parseDefault(text) {
729
+ const trimmed = text.trim();
730
+ try {
731
+ return JSON.parse(trimmed);
732
+ } catch {
733
+ const quoted = /^'(.*)'$/.exec(trimmed) ?? /^`(.*)`$/.exec(trimmed);
734
+ return quoted === null ? trimmed : quoted[1];
735
+ }
736
+ }
737
+ /** Whether a schema is as short as a reference to it would be. */
738
+ function isSmall(schema) {
739
+ if (schema.enum !== void 0 || schema.const !== void 0) return true;
740
+ const keys = Object.keys(schema);
741
+ return keys.length === 0 || keys.length === 1 && (typeof schema.type === "string" || keys[0].startsWith("x-gesso-"));
742
+ }
445
743
  //#endregion
446
744
  //#region bin/gesso-channels.ts
447
745
  /**