@monimesl/dot-cli 0.1.0-alpha.3 → 0.1.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +63 -15
  2. package/dist/cli.js +331 -39
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-z7M4FLjf.js → server-DFOUQnLA.js} +437 -60
  5. package/dist/server-DFOUQnLA.js.map +1 -0
  6. package/dist/{template → templates/showcase}/dot.ts +3 -0
  7. package/dist/templates/showcase/pages/carousel/controller.ts +10 -0
  8. package/dist/templates/showcase/pages/carousel/state.ts +40 -0
  9. package/dist/templates/showcase/pages/carousel/view.dml +37 -0
  10. package/dist/templates/showcase/pages/filters/controller.ts +28 -0
  11. package/dist/templates/showcase/pages/filters/state.ts +72 -0
  12. package/dist/templates/showcase/pages/filters/view.dml +43 -0
  13. package/dist/{template → templates/showcase}/pages/index/state.ts +19 -1
  14. package/dist/{template → templates/showcase}/pages/layout/controller.ts +3 -0
  15. package/dist/{template → templates/showcase}/pages/layout/state.ts +2 -0
  16. package/dist/{template → templates/showcase}/pages/layout/view.dml +17 -0
  17. package/dist/{template → templates/showcase}/pages/lists/view.dml +5 -5
  18. package/dist/{template → templates/showcase}/pages/navigation/controller.ts +22 -0
  19. package/dist/{template → templates/showcase}/pages/navigation/state.ts +4 -0
  20. package/dist/{template → templates/showcase}/pages/navigation/view.dml +10 -0
  21. package/dist/templates/showcase/pages/payments/controller.ts +42 -0
  22. package/dist/templates/showcase/pages/payments/state.ts +10 -0
  23. package/dist/templates/showcase/pages/payments/view.dml +35 -0
  24. package/dist/templates/starter/.env +3 -0
  25. package/dist/templates/starter/dot.ts +3 -0
  26. package/dist/templates/starter/pages/index/controller.ts +9 -0
  27. package/dist/templates/starter/pages/index/state.ts +4 -0
  28. package/dist/templates/starter/pages/index/view.dml +16 -0
  29. package/dist/templates/starter/statics/logo.png +0 -0
  30. package/package.json +8 -7
  31. package/dist/server-z7M4FLjf.js.map +0 -1
  32. /package/dist/{template → templates/showcase}/.env +0 -0
  33. /package/dist/{template → templates/showcase}/pages/device/controller.ts +0 -0
  34. /package/dist/{template → templates/showcase}/pages/device/state.ts +0 -0
  35. /package/dist/{template → templates/showcase}/pages/device/view.dml +0 -0
  36. /package/dist/{template → templates/showcase}/pages/feedback/controller.ts +0 -0
  37. /package/dist/{template → templates/showcase}/pages/feedback/state.ts +0 -0
  38. /package/dist/{template → templates/showcase}/pages/feedback/view.dml +0 -0
  39. /package/dist/{template → templates/showcase}/pages/files/controller.ts +0 -0
  40. /package/dist/{template → templates/showcase}/pages/files/state.ts +0 -0
  41. /package/dist/{template → templates/showcase}/pages/files/view.dml +0 -0
  42. /package/dist/{template → templates/showcase}/pages/forms/controller.ts +0 -0
  43. /package/dist/{template → templates/showcase}/pages/forms/state.ts +0 -0
  44. /package/dist/{template → templates/showcase}/pages/forms/view.dml +0 -0
  45. /package/dist/{template → templates/showcase}/pages/images/controller.ts +0 -0
  46. /package/dist/{template → templates/showcase}/pages/images/state.ts +0 -0
  47. /package/dist/{template → templates/showcase}/pages/images/view.dml +0 -0
  48. /package/dist/{template → templates/showcase}/pages/index/controller.ts +0 -0
  49. /package/dist/{template → templates/showcase}/pages/index/view.dml +0 -0
  50. /package/dist/{template → templates/showcase}/pages/lists/controller.ts +0 -0
  51. /package/dist/{template → templates/showcase}/pages/lists/state.ts +0 -0
  52. /package/dist/{template → templates/showcase}/pages/network/controller.ts +0 -0
  53. /package/dist/{template → templates/showcase}/pages/network/state.ts +0 -0
  54. /package/dist/{template → templates/showcase}/pages/network/view.dml +0 -0
  55. /package/dist/{template → templates/showcase}/pages/state/controller.ts +0 -0
  56. /package/dist/{template → templates/showcase}/pages/state/state.ts +0 -0
  57. /package/dist/{template → templates/showcase}/pages/state/view.dml +0 -0
  58. /package/dist/{template → templates/showcase}/pages/text/controller.ts +0 -0
  59. /package/dist/{template → templates/showcase}/pages/text/state.ts +0 -0
  60. /package/dist/{template → templates/showcase}/pages/text/view.dml +0 -0
  61. /package/dist/{template → templates/showcase}/statics/logo.png +0 -0
@@ -3,9 +3,11 @@ import { dirname, join } from "node:path";
3
3
  import { CONTROL_FLOW_TAGS, DATA_PREFIX, ROOT_TAG, canHaveChildren, compileView, dataKey, listStatics } from "@monimesl/dot-compiler";
4
4
  import { existsSync } from "node:fs";
5
5
  import { readFile } from "node:fs/promises";
6
- import { parseLogicModule, parseStateModule } from "@monimesl/dot-script-parser";
6
+ import { parseDmlToIR } from "@monimesl/dot-dml-parser";
7
+ import { ScriptParseError, parseExpression, parseLogicModule, parseStateModule } from "@monimesl/dot-script-parser";
7
8
  import { SourceError, builtinRegistry, eventName, eventProp, isEventProp } from "@monimesl/dot-core";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import ts from "typescript";
9
11
  //#region \0rolldown/runtime.js
10
12
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
11
13
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
@@ -10141,6 +10143,159 @@ function getWellformedEdit(textEdit) {
10141
10143
  return textEdit;
10142
10144
  }
10143
10145
  //#endregion
10146
+ //#region src/lsp/shapes.ts
10147
+ const NUMBER = {
10148
+ kind: "value",
10149
+ type: "number"
10150
+ };
10151
+ function parseBinding(source) {
10152
+ try {
10153
+ return parseExpression(source);
10154
+ } catch (error) {
10155
+ if (error instanceof ScriptParseError) return;
10156
+ throw error;
10157
+ }
10158
+ }
10159
+ function shapeOf(expr, scope) {
10160
+ switch (expr.type) {
10161
+ case "Identifier":
10162
+ if (scope.loops.has(expr.name)) return scope.loops.get(expr.name);
10163
+ return scope.state[expr.name];
10164
+ case "Member": {
10165
+ const owner = shapeOf(expr.object, scope);
10166
+ if (!owner) return;
10167
+ const { property } = expr;
10168
+ if (property.type === "Literal" && typeof property.value === "string") return membersOf(owner)[property.value];
10169
+ return owner.kind === "list" ? owner.item : void 0;
10170
+ }
10171
+ default: return;
10172
+ }
10173
+ }
10174
+ function membersOf(shape) {
10175
+ switch (shape.kind) {
10176
+ case "object": return shape.properties;
10177
+ case "list": return { length: NUMBER };
10178
+ case "value": return {};
10179
+ }
10180
+ }
10181
+ function describeMembers(shape) {
10182
+ return Object.keys(membersOf(shape)).join(", ") || "none";
10183
+ }
10184
+ function loopScope(variables, state) {
10185
+ const loops = /* @__PURE__ */ new Map();
10186
+ for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER);
10187
+ else loops.set(variable.name, itemShape(variable.list, {
10188
+ state,
10189
+ loops
10190
+ }));
10191
+ return {
10192
+ state,
10193
+ loops
10194
+ };
10195
+ }
10196
+ function itemShape(each, scope) {
10197
+ const list = parseBinding(each.replace(/^\s*{{|}}\s*$/g, ""));
10198
+ const shape = list && shapeOf(list, scope);
10199
+ return shape?.kind === "list" ? shape.item : void 0;
10200
+ }
10201
+ function withLoop(scope, item, itemShape, index) {
10202
+ const loops = new Map(scope.loops).set(item, itemShape).set(index, NUMBER);
10203
+ return {
10204
+ state: scope.state,
10205
+ loops
10206
+ };
10207
+ }
10208
+ //#endregion
10209
+ //#region src/lsp/check-members.ts
10210
+ const BINDING = /{{([\s\S]*?)}}/g;
10211
+ /**
10212
+ * Finds the bindings that read a property their value doesn't have, going by the types in state.ts:
10213
+ * `{{ tap.nmae }}` where each tap is a `Tap`. A value whose type isn't known is not checked.
10214
+ */
10215
+ function checkMembers(text, state) {
10216
+ let nodes;
10217
+ try {
10218
+ nodes = parseDmlToIR(text, {
10219
+ locations: true,
10220
+ whitespace: "preserve"
10221
+ });
10222
+ } catch {
10223
+ return [];
10224
+ }
10225
+ const problems = [];
10226
+ checkNodes(text, nodes, {
10227
+ state,
10228
+ loops: /* @__PURE__ */ new Map()
10229
+ }, problems);
10230
+ return problems;
10231
+ }
10232
+ function checkNodes(text, nodes, scope, problems) {
10233
+ for (const node of nodes) {
10234
+ const from = node.loc?.offset ?? 0;
10235
+ if (node.type === "Text") {
10236
+ checkBindings(text, node.content, from, scope, problems);
10237
+ continue;
10238
+ }
10239
+ for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10240
+ if (node.tag !== "For") {
10241
+ checkNodes(text, node.children, scope, problems);
10242
+ continue;
10243
+ }
10244
+ const { each = "", item = "item", index = "index", key } = node.attributes;
10245
+ const itemOfList = itemShape(each, scope);
10246
+ if (itemOfList?.kind === "object" && key !== void 0 && !key.startsWith("*") && !(key in itemOfList.properties)) problems.push({
10247
+ message: `<For> \`key\` "${key}" is not a property of \`${itemOfList.type}\` (properties: ${describeMembers(itemOfList)})`,
10248
+ offset: offsetOf(text, `key="${key}"`, from)
10249
+ });
10250
+ checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10251
+ }
10252
+ }
10253
+ function checkBindings(text, value, from, scope, problems) {
10254
+ for (const [binding, source = ""] of value.matchAll(BINDING)) {
10255
+ const expr = parseBinding(source);
10256
+ if (!expr) continue;
10257
+ const bindingOffset = offsetOf(text, binding, from);
10258
+ for (const { name, owner } of unknownMembers(expr, scope)) problems.push({
10259
+ message: `\`${name}\` is not a property of \`${owner.type}\` (properties: ${describeMembers(owner)})`,
10260
+ offset: memberOffset(bindingOffset, binding, name)
10261
+ });
10262
+ }
10263
+ }
10264
+ function unknownMembers(expr, scope) {
10265
+ const found = (node) => unknownMembers(node, scope);
10266
+ switch (expr.type) {
10267
+ case "Literal":
10268
+ case "Identifier": return [];
10269
+ case "Member": {
10270
+ const inner = [...found(expr.object), ...found(expr.property)];
10271
+ const { property } = expr;
10272
+ if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10273
+ const owner = shapeOf(expr.object, scope);
10274
+ if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10275
+ name: property.value,
10276
+ owner
10277
+ }];
10278
+ return inner;
10279
+ }
10280
+ case "Unary": return found(expr.argument);
10281
+ case "Binary": return [...found(expr.left), ...found(expr.right)];
10282
+ case "Conditional": return [
10283
+ ...found(expr.test),
10284
+ ...found(expr.consequent),
10285
+ ...found(expr.alternate)
10286
+ ];
10287
+ }
10288
+ }
10289
+ function offsetOf(text, written, from) {
10290
+ const at = text.indexOf(written, from);
10291
+ return at < 0 ? from : at;
10292
+ }
10293
+ function memberOffset(bindingOffset, binding, name) {
10294
+ const escapedName = name.replaceAll("$", "\\$");
10295
+ const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10296
+ return match ? bindingOffset + match.index : bindingOffset;
10297
+ }
10298
+ //#endregion
10144
10299
  //#region src/lsp/markup.ts
10145
10300
  const NAME = /[A-Za-z0-9_.:-]/;
10146
10301
  const ATTRIBUTE_NAME_END = /[\s=/>"']/;
@@ -10253,7 +10408,8 @@ function readTag(text, start, offset, open) {
10253
10408
  kind: "attributeValue",
10254
10409
  start: valueStart,
10255
10410
  element,
10256
- attribute: name
10411
+ attribute: name,
10412
+ loopVariables: loopVariables(open)
10257
10413
  },
10258
10414
  end: valueEnd
10259
10415
  };
@@ -10263,17 +10419,19 @@ function readTag(text, start, offset, open) {
10263
10419
  }
10264
10420
  }
10265
10421
  function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10422
+ const variables = loopVariables(open);
10266
10423
  const bindingStart = valueBeforeCursor.lastIndexOf("{{");
10267
10424
  if (bindingStart >= 0 && !valueBeforeCursor.includes("}}", bindingStart)) return {
10268
10425
  kind: "binding",
10269
10426
  start: valueStart + bindingStart + 2,
10270
- loopVariables: loopVariables(open)
10427
+ loopVariables: variables
10271
10428
  };
10272
10429
  return {
10273
10430
  kind: "attributeValue",
10274
10431
  start: valueStart,
10275
10432
  element,
10276
- attribute
10433
+ attribute,
10434
+ loopVariables: variables
10277
10435
  };
10278
10436
  }
10279
10437
  function closeElement(open, tag) {
@@ -10323,6 +10481,7 @@ const TAG_NAME = /[A-Za-z0-9_.:-]/;
10323
10481
  const ATTRIBUTE_NAME = /[^\s=/>"']/;
10324
10482
  const IMAGE_ATTRIBUTES = ["src", "image"];
10325
10483
  const IDENTIFIER = /[\w$]/;
10484
+ const MEMBER_CHAIN = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\.\s*$/;
10326
10485
  const VALUE = /[^\s"']/;
10327
10486
  const CONTROL_FLOW_SNIPPETS = {
10328
10487
  If: "If condition=\"{{ $1 }}\">$0</If>",
@@ -10375,21 +10534,24 @@ function complete(document, offset, page, { components }) {
10375
10534
  newText: item.insertText ?? item.label
10376
10535
  }
10377
10536
  }));
10378
- case "attributeValue": return valueCompletions(context.element, context.attribute, page, components).map((item) => ({
10379
- ...item,
10380
- textEdit: {
10381
- range: range(context.start),
10382
- newText: item.label
10383
- }
10384
- }));
10537
+ case "attributeValue": {
10538
+ const scope = shapeScope(context.loopVariables, page);
10539
+ return valueCompletions(context.element, context.attribute, page, scope, components).map((item) => ({
10540
+ ...item,
10541
+ textEdit: {
10542
+ range: range(context.start),
10543
+ newText: item.label
10544
+ }
10545
+ }));
10546
+ }
10385
10547
  case "binding": {
10386
10548
  const typed = text.slice(context.start, offset);
10387
- const identifier = /[A-Za-z_$][\w$]*$/.exec(typed)?.[0] ?? "";
10388
- if (typed.slice(0, typed.length - identifier.length).trimEnd().endsWith(".")) return [];
10389
- return bindingCompletions(context.loopVariables, page).map((item) => ({
10549
+ const wordStart = offset - (/[A-Za-z_$][\w$]*$/.exec(typed)?.[0] ?? "").length;
10550
+ const scope = shapeScope(context.loopVariables, page);
10551
+ return (isMember(text, context.start, wordStart) ? memberCompletions(ownerOf(text, context.start, wordStart, scope)) : bindingCompletions(context.loopVariables, page, scope)).map((item) => ({
10390
10552
  ...item,
10391
10553
  textEdit: {
10392
- range: range(offset - identifier.length),
10554
+ range: range(wordStart),
10393
10555
  newText: item.label
10394
10556
  }
10395
10557
  }));
@@ -10464,16 +10626,23 @@ function attributeCompletions(element, components) {
10464
10626
  }));
10465
10627
  return [...props, ...events];
10466
10628
  }
10467
- function valueCompletions(element, attribute, page, components) {
10468
- if (element.tag === "For" && attribute === "key") return [{
10469
- label: "*this",
10470
- kind: import_main.CompletionItemKind.Value,
10471
- detail: "the item itself"
10472
- }, {
10473
- label: "*index",
10474
- kind: import_main.CompletionItemKind.Value,
10475
- detail: "match items by position"
10476
- }];
10629
+ function valueCompletions(element, attribute, page, scope, components) {
10630
+ if (element.tag === "For" && attribute === "key") {
10631
+ const item = scope && itemShape(element.attributes.each ?? "", scope);
10632
+ return [
10633
+ ...item?.kind === "object" ? memberCompletions(item) : [],
10634
+ {
10635
+ label: "*this",
10636
+ kind: import_main.CompletionItemKind.Value,
10637
+ detail: "the item itself"
10638
+ },
10639
+ {
10640
+ label: "*index",
10641
+ kind: import_main.CompletionItemKind.Value,
10642
+ detail: "match items by position"
10643
+ }
10644
+ ];
10645
+ }
10477
10646
  const component = components[element.tag];
10478
10647
  if (!component) return [];
10479
10648
  if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerItems(page);
@@ -10505,22 +10674,42 @@ function handlerItems(page) {
10505
10674
  detail: "action"
10506
10675
  }))];
10507
10676
  }
10508
- function bindingCompletions(loopVariables, page) {
10677
+ function bindingCompletions(loopVariables, page, scope) {
10509
10678
  const items = /* @__PURE__ */ new Map();
10510
10679
  for (const key of page.state?.keys ?? []) items.set(key, {
10511
10680
  label: key,
10512
10681
  kind: import_main.CompletionItemKind.Field,
10513
- detail: "state",
10682
+ detail: scope?.state[key] ? `state: ${scope.state[key].type}` : "state",
10514
10683
  sortText: `1${key}`
10515
10684
  });
10516
10685
  for (const variable of loopVariables) items.set(variable.name, {
10517
10686
  label: variable.name,
10518
10687
  kind: import_main.CompletionItemKind.Variable,
10519
- detail: describeLoopVariable(variable),
10688
+ detail: describeLoopVariable(variable, scope?.loops.get(variable.name)),
10520
10689
  sortText: `0${variable.name}`
10521
10690
  });
10522
10691
  return [...items.values()];
10523
10692
  }
10693
+ function memberCompletions(owner) {
10694
+ if (!owner) return [];
10695
+ return Object.entries(membersOf(owner)).map(([name, shape]) => ({
10696
+ label: name,
10697
+ kind: import_main.CompletionItemKind.Field,
10698
+ detail: shape.type
10699
+ }));
10700
+ }
10701
+ function shapeScope(loopVariables, page) {
10702
+ const shapes = page.state?.shapes;
10703
+ return shapes && loopScope(loopVariables, shapes);
10704
+ }
10705
+ function isMember(text, bindingStart, start) {
10706
+ return text.slice(bindingStart, start).trimEnd().endsWith(".");
10707
+ }
10708
+ function ownerOf(text, bindingStart, start, scope) {
10709
+ const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, start))?.[1];
10710
+ const expr = chain === void 0 ? void 0 : parseBinding(chain);
10711
+ return expr && scope && shapeOf(expr, scope);
10712
+ }
10524
10713
  function hover(document, offset, page, { components }) {
10525
10714
  const found = symbolAt(document.getText(), offset, page);
10526
10715
  if (!found) return null;
@@ -10543,6 +10732,20 @@ function definition(document, offset, page) {
10543
10732
  const { symbol } = found;
10544
10733
  if (symbol.kind === "handler" && page.controller) return locationIn(page.controller.file, page.controller.text, page.controller.offsets[symbol.name]);
10545
10734
  if (symbol.kind === "state" && page.state) return locationIn(page.state.file, page.state.text, page.state.offsets[symbol.name]);
10735
+ if (symbol.kind === "property" && symbol.shape.declaration) {
10736
+ const { file, line, character } = symbol.shape.declaration;
10737
+ const position = {
10738
+ line,
10739
+ character
10740
+ };
10741
+ return {
10742
+ uri: pathToFileURL(file).href,
10743
+ range: {
10744
+ start: position,
10745
+ end: position
10746
+ }
10747
+ };
10748
+ }
10546
10749
  if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
10547
10750
  return null;
10548
10751
  }
@@ -10598,16 +10801,38 @@ function symbolAt(text, offset, page) {
10598
10801
  case "binding": {
10599
10802
  const { start, end } = wordAt(text, offset, IDENTIFIER);
10600
10803
  const name = text.slice(start, end);
10601
- if (!name || text.slice(context.start, start).trimEnd().endsWith(".")) return null;
10804
+ if (!name) return null;
10805
+ const scope = shapeScope(context.loopVariables, page);
10806
+ if (isMember(text, context.start, start)) {
10807
+ const owner = ownerOf(text, context.start, start, scope);
10808
+ const shape = owner && membersOf(owner)[name];
10809
+ return owner && shape ? {
10810
+ symbol: {
10811
+ kind: "property",
10812
+ name,
10813
+ owner,
10814
+ shape
10815
+ },
10816
+ start,
10817
+ end
10818
+ } : null;
10819
+ }
10602
10820
  const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
10603
- if (variable) return {
10604
- symbol: {
10605
- kind: "loopVariable",
10606
- variable
10607
- },
10608
- start,
10609
- end
10610
- };
10821
+ if (variable) {
10822
+ const shape = scope?.loops.get(name);
10823
+ return {
10824
+ symbol: shape ? {
10825
+ kind: "loopVariable",
10826
+ variable,
10827
+ shape
10828
+ } : {
10829
+ kind: "loopVariable",
10830
+ variable
10831
+ },
10832
+ start,
10833
+ end
10834
+ };
10835
+ }
10611
10836
  return {
10612
10837
  symbol: {
10613
10838
  kind: "state",
@@ -10647,8 +10872,9 @@ function describeSymbol(symbol, page, components) {
10647
10872
  if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
10648
10873
  return null;
10649
10874
  }
10650
- case "state": return page.state?.keys?.includes(symbol.name) ? `state \`${symbol.name}\` in state.ts` : null;
10651
- case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable)}`;
10875
+ case "state": return page.state?.keys?.includes(symbol.name) ? `state \`${symbol.name}\` in state.ts${typeSuffix(page.state.shapes?.[symbol.name])}` : null;
10876
+ case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
10877
+ case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
10652
10878
  case "static": return `image \`${symbol.path}\``;
10653
10879
  }
10654
10880
  }
@@ -10670,9 +10896,12 @@ function describeType(type) {
10670
10896
  if (typeof type === "string") return type;
10671
10897
  return type.map((value) => `"${value}"`).join(" | ");
10672
10898
  }
10673
- function describeLoopVariable(variable) {
10899
+ function describeLoopVariable(variable, shape) {
10674
10900
  const list = variable.list ? ` \`${variable.list}\`` : "";
10675
- return variable.role === "item" ? `each item of${list}` : `the position in${list}`;
10901
+ return variable.role === "item" ? `each item of${list}${typeSuffix(shape)}` : `the position in${list}`;
10902
+ }
10903
+ function typeSuffix(shape) {
10904
+ return shape ? `: \`${shape.type}\`` : "";
10676
10905
  }
10677
10906
  function locationIn(file, text, offset) {
10678
10907
  if (offset === void 0) return null;
@@ -10690,7 +10919,7 @@ function diagnose(document, file, page, { components }) {
10690
10919
  const context = {
10691
10920
  file,
10692
10921
  components,
10693
- onWarning: (warning) => diagnostics.push(diagnosticAt(document, warning, import_main.DiagnosticSeverity.Warning))
10922
+ onWarning: (warning) => diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning))
10694
10923
  };
10695
10924
  if (page.statics) context.statics = page.statics;
10696
10925
  if (page.state?.keys) context.stateKeys = page.state.keys;
@@ -10700,19 +10929,22 @@ function diagnose(document, file, page, { components }) {
10700
10929
  compileView(document.getText(), context);
10701
10930
  } catch (error) {
10702
10931
  if (!(error instanceof SourceError)) throw error;
10703
- diagnostics.push(diagnosticAt(document, error, import_main.DiagnosticSeverity.Error));
10932
+ diagnostics.push(sourceDiagnostic(document, error, import_main.DiagnosticSeverity.Error));
10704
10933
  }
10934
+ if (page.state?.shapes) for (const problem of checkMembers(document.getText(), page.state.shapes)) diagnostics.push(diagnosticAt(document, problem.offset, problem.message, import_main.DiagnosticSeverity.Error));
10705
10935
  return diagnostics;
10706
10936
  }
10707
- function diagnosticAt(document, problem, severity) {
10937
+ function sourceDiagnostic(document, problem, severity) {
10938
+ return diagnosticAt(document, problem.loc?.offset ?? 0, problem.reason, severity);
10939
+ }
10940
+ function diagnosticAt(document, start, message, severity) {
10708
10941
  const text = document.getText();
10709
- const start = problem.loc?.offset ?? 0;
10710
10942
  let end = start;
10711
10943
  while (end < text.length && !/[\s>]/.test(text[end])) end++;
10712
10944
  return {
10713
10945
  severity,
10714
10946
  source: "dot",
10715
- message: problem.reason,
10947
+ message,
10716
10948
  range: {
10717
10949
  start: document.positionAt(start),
10718
10950
  end: document.positionAt(Math.max(end, Math.min(start + 1, text.length)))
@@ -10721,11 +10953,11 @@ function diagnosticAt(document, problem, severity) {
10721
10953
  }
10722
10954
  //#endregion
10723
10955
  //#region src/lsp/page.ts
10724
- async function readPage(viewFile, readText) {
10956
+ async function readPage(viewFile, readText, stateTypes) {
10725
10957
  const dir = dirname(viewFile);
10726
10958
  const root = findRoot(dir);
10727
10959
  const [state, controller, statics] = await Promise.all([
10728
- readState(join(dir, "state.ts"), readText),
10960
+ readState(join(dir, "state.ts"), root, readText, stateTypes),
10729
10961
  readController(join(dir, "controller.ts"), readText),
10730
10962
  root === void 0 ? void 0 : listStatics(root).catch(() => void 0)
10731
10963
  ]);
@@ -10736,17 +10968,27 @@ async function readPage(viewFile, readText) {
10736
10968
  statics
10737
10969
  };
10738
10970
  }
10739
- async function readState(file, readText) {
10971
+ async function readState(file, root, readText, stateTypes) {
10740
10972
  const text = await readText(file);
10741
10973
  if (text === void 0) return;
10974
+ let keys;
10975
+ let offsets;
10742
10976
  try {
10743
- const { keys, keyOffsets } = parseStateModule(text, file);
10744
- return {
10745
- file,
10746
- text,
10747
- keys,
10748
- offsets: keyOffsets
10749
- };
10977
+ ({keys, keyOffsets: offsets} = parseStateModule(text, file));
10978
+ } catch {
10979
+ return;
10980
+ }
10981
+ return {
10982
+ file,
10983
+ text,
10984
+ keys,
10985
+ offsets,
10986
+ shapes: readShapes(stateTypes, root, file, text)
10987
+ };
10988
+ }
10989
+ function readShapes(stateTypes, root, file, text) {
10990
+ try {
10991
+ return stateTypes.read(root, file, text);
10750
10992
  } catch {
10751
10993
  return;
10752
10994
  }
@@ -10776,10 +11018,144 @@ function findRoot(dir) {
10776
11018
  }
10777
11019
  }
10778
11020
  //#endregion
11021
+ //#region src/lsp/state-types.ts
11022
+ const MAX_DEPTH = 8;
11023
+ const FALLBACK_OPTIONS = {
11024
+ target: ts.ScriptTarget.ES2022,
11025
+ module: ts.ModuleKind.ESNext,
11026
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
11027
+ strict: true,
11028
+ noEmit: true,
11029
+ skipLibCheck: true
11030
+ };
11031
+ /**
11032
+ * Reads the shapes of state.ts files. Each dot keeps one TypeScript language service, so reading
11033
+ * again after an edit rechecks only what changed.
11034
+ */
11035
+ var StateTypes = class {
11036
+ #projects = /* @__PURE__ */ new Map();
11037
+ read(root, file, text) {
11038
+ const key = root ?? file;
11039
+ let project = this.#projects.get(key);
11040
+ if (!project) {
11041
+ project = new Project(root);
11042
+ this.#projects.set(key, project);
11043
+ }
11044
+ return project.read(file, text);
11045
+ }
11046
+ };
11047
+ var Project = class {
11048
+ #options;
11049
+ #fileNames;
11050
+ #open = /* @__PURE__ */ new Map();
11051
+ #service;
11052
+ constructor(root) {
11053
+ const config = root === void 0 ? void 0 : readConfig(root);
11054
+ this.#options = config?.options ?? FALLBACK_OPTIONS;
11055
+ this.#fileNames = config?.fileNames ?? [];
11056
+ const host = {
11057
+ getCompilationSettings: () => this.#options,
11058
+ getScriptFileNames: () => [.../* @__PURE__ */ new Set([...this.#fileNames, ...this.#open.keys()])],
11059
+ getScriptVersion: (file) => {
11060
+ const open = this.#open.get(file);
11061
+ if (open) return String(open.version);
11062
+ return String(ts.sys.getModifiedTime?.(file)?.getTime() ?? 0);
11063
+ },
11064
+ getScriptSnapshot: (file) => {
11065
+ const text = this.#open.get(file)?.text ?? ts.sys.readFile(file);
11066
+ return text === void 0 ? void 0 : ts.ScriptSnapshot.fromString(text);
11067
+ },
11068
+ getCurrentDirectory: () => root ?? process.cwd(),
11069
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
11070
+ fileExists: (file) => ts.sys.fileExists(file),
11071
+ readFile: (file) => ts.sys.readFile(file),
11072
+ readDirectory: (...args) => ts.sys.readDirectory(...args),
11073
+ directoryExists: (dir) => ts.sys.directoryExists(dir),
11074
+ getDirectories: (dir) => ts.sys.getDirectories(dir),
11075
+ realpath: (path) => ts.sys.realpath?.(path) ?? path
11076
+ };
11077
+ this.#service = ts.createLanguageService(host);
11078
+ }
11079
+ read(file, text) {
11080
+ const open = this.#open.get(file);
11081
+ if (open?.text !== text) this.#open.set(file, {
11082
+ text,
11083
+ version: (open?.version ?? 0) + 1
11084
+ });
11085
+ const program = this.#service.getProgram();
11086
+ const source = program?.getSourceFile(file);
11087
+ if (!program || !source) return;
11088
+ const checker = program.getTypeChecker();
11089
+ const module = checker.getSymbolAtLocation(source);
11090
+ let exported = module && checker.tryGetMemberInModuleExports("default", module);
11091
+ if (!exported) return;
11092
+ if (exported.flags & ts.SymbolFlags.Alias) exported = checker.getAliasedSymbol(exported);
11093
+ const state = checker.getTypeOfSymbol(exported);
11094
+ const shapes = {};
11095
+ for (const property of checker.getPropertiesOfType(state)) shapes[property.name] = propertyShape(checker, property, 0, /* @__PURE__ */ new Set());
11096
+ return shapes;
11097
+ }
11098
+ };
11099
+ function readConfig(root) {
11100
+ const file = join(root, "tsconfig.json");
11101
+ if (!existsSync(file)) return;
11102
+ const read = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
11103
+ if (read.error) return;
11104
+ return ts.parseJsonConfigFileContent(read.config, ts.sys, root, void 0, file);
11105
+ }
11106
+ function propertyShape(checker, property, depth, seen) {
11107
+ const shape = typeShape(checker, checker.getTypeOfSymbol(property), depth, seen);
11108
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
11109
+ if (!declaration) return shape;
11110
+ const source = declaration.getSourceFile();
11111
+ const { line, character } = source.getLineAndCharacterOfPosition(declaration.getStart());
11112
+ return {
11113
+ ...shape,
11114
+ declaration: {
11115
+ file: source.fileName,
11116
+ line,
11117
+ character
11118
+ }
11119
+ };
11120
+ }
11121
+ function typeShape(checker, type, depth, seen) {
11122
+ const present = checker.getNonNullableType(type);
11123
+ const name = checker.typeToString(present);
11124
+ if (depth >= MAX_DEPTH || seen.has(present) || present.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return {
11125
+ kind: "value",
11126
+ type: name
11127
+ };
11128
+ const inner = new Set(seen).add(present);
11129
+ if (checker.isArrayType(present) || checker.isTupleType(present)) {
11130
+ const item = checker.getIndexTypeOfType(present, ts.IndexKind.Number);
11131
+ return {
11132
+ kind: "list",
11133
+ type: name,
11134
+ item: item ? typeShape(checker, item, depth + 1, inner) : {
11135
+ kind: "value",
11136
+ type: "unknown"
11137
+ }
11138
+ };
11139
+ }
11140
+ if (!isRecord(checker, present)) return {
11141
+ kind: "value",
11142
+ type: name
11143
+ };
11144
+ const properties = {};
11145
+ for (const property of checker.getPropertiesOfType(present)) properties[property.name] = propertyShape(checker, property, depth + 1, inner);
11146
+ return {
11147
+ kind: "object",
11148
+ type: name,
11149
+ properties
11150
+ };
11151
+ }
11152
+ function isRecord(checker, type) {
11153
+ return (type.isUnion() ? type.types : [type]).every((member) => (member.flags & ts.TypeFlags.Object) !== 0 && member.getCallSignatures().length === 0 && checker.getIndexInfosOfType(member).length === 0);
11154
+ }
11155
+ //#endregion
10779
11156
  //#region src/lsp/server.ts
10780
11157
  const WATCHED = [
10781
- "**/state.ts",
10782
- "**/controller.ts",
11158
+ "**/*.ts",
10783
11159
  "**/dot.json",
10784
11160
  "**/statics/**"
10785
11161
  ];
@@ -10791,6 +11167,7 @@ const WATCHED = [
10791
11167
  function startLanguageServer(connection) {
10792
11168
  const documents = new import_main.TextDocuments(TextDocument);
10793
11169
  const features = { components: builtinRegistry };
11170
+ const stateTypes = new StateTypes();
10794
11171
  let canWatchFiles = false;
10795
11172
  const readText = async (file) => {
10796
11173
  const open = documents.get(pathToFileURL(file).href);
@@ -10803,7 +11180,7 @@ function startLanguageServer(connection) {
10803
11180
  const file = fileURLToPath(uri);
10804
11181
  return {
10805
11182
  document,
10806
- page: await readPage(file, readText),
11183
+ page: await readPage(file, readText, stateTypes),
10807
11184
  file
10808
11185
  };
10809
11186
  };
@@ -10883,4 +11260,4 @@ function serveStdio() {
10883
11260
  //#endregion
10884
11261
  export { serveStdio };
10885
11262
 
10886
- //# sourceMappingURL=server-z7M4FLjf.js.map
11263
+ //# sourceMappingURL=server-DFOUQnLA.js.map