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

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 (159) hide show
  1. package/README.md +91 -17
  2. package/dist/cli.js +776 -199
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-z7M4FLjf.js → server-DMnWDdJ6.js} +702 -280
  5. package/dist/server-DMnWDdJ6.js.map +1 -0
  6. package/dist/templates/showcase/.env +3 -0
  7. package/dist/templates/showcase/dot.ts +60 -0
  8. package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
  9. package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
  10. package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
  11. package/dist/templates/showcase/pages/basics/bindings/controller.ts +15 -0
  12. package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
  13. package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
  14. package/dist/templates/showcase/pages/basics/colors/controller.ts +112 -0
  15. package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
  16. package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
  17. package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
  18. package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
  19. package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
  20. package/dist/templates/showcase/pages/basics/concert/view.dml +20 -0
  21. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
  22. package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
  23. package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
  24. package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
  25. package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
  26. package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
  27. package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
  28. package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
  29. package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
  30. package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
  31. package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
  32. package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -0
  33. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +18 -0
  34. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  35. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
  36. package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
  37. package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
  38. package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
  39. package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
  40. package/dist/{template/pages → templates/showcase/pages/basics}/state/controller.ts +1 -0
  41. package/dist/{template/pages → templates/showcase/pages/basics}/state/state.ts +1 -1
  42. package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
  43. package/dist/templates/showcase/pages/components/carousel/controller.ts +41 -0
  44. package/dist/templates/showcase/pages/components/carousel/state.ts +77 -0
  45. package/dist/templates/showcase/pages/components/carousel/view.dml +42 -0
  46. package/dist/templates/showcase/pages/components/feedback/controller.ts +32 -0
  47. package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
  48. package/dist/templates/showcase/pages/components/feedback/view.dml +116 -0
  49. package/dist/templates/showcase/pages/components/forms/controller.ts +72 -0
  50. package/dist/templates/showcase/pages/components/forms/state.ts +37 -0
  51. package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
  52. package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
  53. package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
  54. package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
  55. package/dist/templates/showcase/pages/components/images/controller.ts +44 -0
  56. package/dist/{template/pages → templates/showcase/pages/components}/images/state.ts +3 -0
  57. package/dist/templates/showcase/pages/components/images/view.dml +83 -0
  58. package/dist/templates/showcase/pages/components/layout/controller.ts +61 -0
  59. package/dist/templates/showcase/pages/components/layout/state.ts +34 -0
  60. package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
  61. package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
  62. package/dist/templates/showcase/pages/components/lists/state.ts +130 -0
  63. package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
  64. package/dist/templates/showcase/pages/components/money/controller.ts +45 -0
  65. package/dist/templates/showcase/pages/components/money/state.ts +13 -0
  66. package/dist/templates/showcase/pages/components/money/view.dml +54 -0
  67. package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
  68. package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
  69. package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
  70. package/dist/templates/showcase/pages/components/sheets/controller.ts +36 -0
  71. package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
  72. package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
  73. package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
  74. package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
  75. package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
  76. package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
  77. package/dist/templates/showcase/pages/components/tabs/state.ts +127 -0
  78. package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
  79. package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
  80. package/dist/templates/showcase/pages/components/text/state.ts +3 -0
  81. package/dist/templates/showcase/pages/components/text/view.dml +95 -0
  82. package/dist/templates/showcase/pages/dash/account/controller.ts +32 -0
  83. package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
  84. package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
  85. package/dist/templates/showcase/pages/dash/cache/controller.ts +50 -0
  86. package/dist/templates/showcase/pages/dash/cache/state.ts +17 -0
  87. package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
  88. package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
  89. package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
  90. package/dist/templates/showcase/pages/dash/calls/view.dml +69 -0
  91. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  92. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  93. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  94. package/dist/templates/showcase/pages/dash/crypto/controller.ts +38 -0
  95. package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
  96. package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
  97. package/dist/templates/showcase/pages/dash/files/controller.ts +71 -0
  98. package/dist/templates/showcase/pages/dash/files/state.ts +23 -0
  99. package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
  100. package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
  101. package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
  102. package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
  103. package/dist/templates/showcase/pages/dash/network/controller.ts +215 -0
  104. package/dist/templates/showcase/pages/dash/network/state.ts +35 -0
  105. package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
  106. package/dist/templates/showcase/pages/dash/secrets/controller.ts +44 -0
  107. package/dist/templates/showcase/pages/dash/secrets/state.ts +5 -0
  108. package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
  109. package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
  110. package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
  111. package/dist/templates/showcase/pages/dash/system/view.dml +96 -0
  112. package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
  113. package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
  114. package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
  115. package/dist/{template → templates/showcase}/pages/index/controller.ts +1 -1
  116. package/dist/templates/showcase/pages/index/state.ts +237 -0
  117. package/dist/templates/showcase/pages/index/view.dml +23 -0
  118. package/dist/templates/showcase/statics/logo.png +0 -0
  119. package/dist/{template → templates/starter}/.env +1 -1
  120. package/dist/templates/starter/dot.ts +3 -0
  121. package/dist/templates/starter/pages/index/controller.ts +9 -0
  122. package/dist/templates/starter/pages/index/state.ts +4 -0
  123. package/dist/templates/starter/pages/index/view.dml +16 -0
  124. package/package.json +8 -7
  125. package/dist/server-z7M4FLjf.js.map +0 -1
  126. package/dist/template/dot.ts +0 -19
  127. package/dist/template/pages/device/controller.ts +0 -70
  128. package/dist/template/pages/device/state.ts +0 -7
  129. package/dist/template/pages/device/view.dml +0 -44
  130. package/dist/template/pages/feedback/controller.ts +0 -16
  131. package/dist/template/pages/feedback/state.ts +0 -7
  132. package/dist/template/pages/feedback/view.dml +0 -87
  133. package/dist/template/pages/files/controller.ts +0 -47
  134. package/dist/template/pages/files/state.ts +0 -11
  135. package/dist/template/pages/files/view.dml +0 -35
  136. package/dist/template/pages/forms/controller.ts +0 -44
  137. package/dist/template/pages/forms/state.ts +0 -15
  138. package/dist/template/pages/forms/view.dml +0 -39
  139. package/dist/template/pages/images/controller.ts +0 -15
  140. package/dist/template/pages/images/view.dml +0 -38
  141. package/dist/template/pages/index/state.ts +0 -98
  142. package/dist/template/pages/index/view.dml +0 -20
  143. package/dist/template/pages/layout/controller.ts +0 -23
  144. package/dist/template/pages/layout/state.ts +0 -12
  145. package/dist/template/pages/layout/view.dml +0 -63
  146. package/dist/template/pages/lists/controller.ts +0 -36
  147. package/dist/template/pages/lists/state.ts +0 -96
  148. package/dist/template/pages/lists/view.dml +0 -59
  149. package/dist/template/pages/navigation/controller.ts +0 -42
  150. package/dist/template/pages/navigation/state.ts +0 -13
  151. package/dist/template/pages/navigation/view.dml +0 -24
  152. package/dist/template/pages/network/controller.ts +0 -107
  153. package/dist/template/pages/network/state.ts +0 -15
  154. package/dist/template/pages/network/view.dml +0 -56
  155. package/dist/template/pages/state/view.dml +0 -33
  156. package/dist/template/pages/text/controller.ts +0 -10
  157. package/dist/template/pages/text/state.ts +0 -7
  158. package/dist/template/pages/text/view.dml +0 -49
  159. /package/dist/{template → templates/starter}/statics/logo.png +0 -0
@@ -1,11 +1,14 @@
1
+ import { t as findDotRoot } from "./cli.js";
1
2
  import { createRequire } from "node:module";
2
3
  import { dirname, join } from "node:path";
3
4
  import { CONTROL_FLOW_TAGS, DATA_PREFIX, ROOT_TAG, canHaveChildren, compileView, dataKey, listStatics } from "@monimesl/dot-compiler";
4
5
  import { existsSync } from "node:fs";
5
6
  import { readFile } from "node:fs/promises";
6
- import { parseLogicModule, parseStateModule } from "@monimesl/dot-script-parser";
7
- import { SourceError, builtinRegistry, eventName, eventProp, isEventProp } from "@monimesl/dot-core";
7
+ import { parseDmlToIR } from "@monimesl/dot-dml-parser";
8
+ import { ScriptParseError, parseExpression, parseLogicModule, parseStateModule } from "@monimesl/dot-script-parser";
9
+ import { SourceError, builtinRegistry, eventName, eventProp, isEventProp, isWordsOr, otherThanWords } from "@monimesl/dot-core";
8
10
  import { fileURLToPath, pathToFileURL } from "node:url";
11
+ import ts from "typescript";
9
12
  //#region \0rolldown/runtime.js
10
13
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
11
14
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
@@ -10141,12 +10144,57 @@ function getWellformedEdit(textEdit) {
10141
10144
  return textEdit;
10142
10145
  }
10143
10146
  //#endregion
10147
+ //#region src/lsp/descriptions.ts
10148
+ const CONTROL_FLOW_DOCS = {
10149
+ If: "Shows its children when `condition` is true.",
10150
+ ElseIf: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false and `condition` is true.",
10151
+ Else: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false.",
10152
+ For: "Repeats its children for each item in `each`. Inside, the item is named `item` and its position `index`. `key` tells items apart: a property of each item, `*this` for the item itself, or `*index` to match items by position."
10153
+ };
10154
+ const CONTROL_FLOW_ATTRIBUTE_DOCS = {
10155
+ condition: "A single `{{ }}` binding",
10156
+ each: "The list to repeat, as a single `{{ }}` binding",
10157
+ item: "The name for each item (default `item`)",
10158
+ index: "The name for each position (default `index`)",
10159
+ key: "A property of each item, `*this` or `*index`"
10160
+ };
10161
+ function describeComponent(component) {
10162
+ const lines = [`**<${component.name}>** · ${describeChildren(component)}`];
10163
+ const props = Object.entries(component.props);
10164
+ if (props.length > 0) lines.push("", ...props.map(([name, type]) => `- \`${name}\`: ${describeType(type)}`));
10165
+ if (component.events.length > 0) {
10166
+ const eventProps = component.events.map((event) => `\`${eventProp(event)}\``);
10167
+ lines.push("", `Events: ${eventProps.join(", ")}`);
10168
+ }
10169
+ return lines.join("\n");
10170
+ }
10171
+ function describeType(type) {
10172
+ if (typeof type === "string") return type;
10173
+ if (isWordsOr(type)) return `${describeType(type.words)} | ${otherThanWords(type)}`;
10174
+ return type.map((value) => `"${value}"`).join(" | ");
10175
+ }
10176
+ function describeLoopVariable(variable, shape) {
10177
+ const list = variable.list ? ` \`${variable.list}\`` : "";
10178
+ if (variable.role === "item") return `each item of${list}${typeSuffix(shape)}`;
10179
+ return `the position in${list}`;
10180
+ }
10181
+ function typeSuffix(shape) {
10182
+ return shape ? `: \`${shape.type}\`` : "";
10183
+ }
10184
+ function describeChildren(component) {
10185
+ switch (component.children) {
10186
+ case "none": return "no children";
10187
+ case "text": return "text and inline components";
10188
+ case "elements": return "any components";
10189
+ }
10190
+ }
10191
+ //#endregion
10144
10192
  //#region src/lsp/markup.ts
10145
- const NAME = /[A-Za-z0-9_.:-]/;
10146
- const ATTRIBUTE_NAME_END = /[\s=/>"']/;
10193
+ const TAG_NAME = /[A-Za-z0-9_.:-]/;
10194
+ const ATTRIBUTE_NAME = /[^\s=/>"']/;
10147
10195
  const WHITESPACE = /\s/;
10148
10196
  function markupContextAt(text, offset) {
10149
- const open = [];
10197
+ const openElements = [];
10150
10198
  let index = 0;
10151
10199
  while (index < offset) if (text.startsWith("<!--", index)) {
10152
10200
  const end = text.indexOf("-->", index + 4);
@@ -10154,39 +10202,39 @@ function markupContextAt(text, offset) {
10154
10202
  index = end + 3;
10155
10203
  } else if (text.startsWith("</", index)) {
10156
10204
  const nameStart = index + 2;
10157
- const nameEnd = readWhile(text, nameStart, NAME);
10205
+ const nameEnd = readWhile(text, nameStart, TAG_NAME);
10158
10206
  const close = text.indexOf(">", nameEnd);
10159
10207
  if (offset <= nameEnd || close < 0 || close >= offset) return {
10160
10208
  kind: "closeTag",
10161
10209
  start: nameStart,
10162
- open: open.at(-1)
10210
+ open: openElements.at(-1)
10163
10211
  };
10164
- closeElement(open, text.slice(nameStart, nameEnd));
10212
+ closeElement(openElements, text.slice(nameStart, nameEnd));
10165
10213
  index = close + 1;
10166
10214
  } else if (text[index] === "<") {
10167
- const tag = readTag(text, index, offset, open);
10215
+ const tag = readTag(text, index, offset, openElements);
10168
10216
  if (tag.context) return tag.context;
10169
- if (tag.element && !tag.selfClosing) open.push(tag.element);
10217
+ if (tag.element && !tag.selfClosing) openElements.push(tag.element);
10170
10218
  index = tag.end;
10171
10219
  } else if (text.startsWith("{{", index)) {
10172
10220
  const end = text.indexOf("}}", index + 2);
10173
10221
  if (end < 0 || offset <= end) return {
10174
10222
  kind: "binding",
10175
10223
  start: index + 2,
10176
- loopVariables: loopVariables(open)
10224
+ loopVariables: loopVariables(openElements)
10177
10225
  };
10178
10226
  index = end + 2;
10179
10227
  } else index++;
10180
10228
  return { kind: "text" };
10181
10229
  }
10182
- function readTag(text, start, offset, open) {
10230
+ function readTag(text, start, offset, openElements) {
10183
10231
  const nameStart = start + 1;
10184
- const nameEnd = readWhile(text, nameStart, NAME);
10232
+ const nameEnd = readWhile(text, nameStart, TAG_NAME);
10185
10233
  if (offset <= nameEnd) return {
10186
10234
  context: {
10187
10235
  kind: "tagName",
10188
10236
  start: nameStart,
10189
- parents: [...open]
10237
+ parents: [...openElements]
10190
10238
  },
10191
10239
  end: nameEnd
10192
10240
  };
@@ -10221,7 +10269,7 @@ function readTag(text, start, offset, open) {
10221
10269
  end: index + 1
10222
10270
  };
10223
10271
  const attributeStart = index;
10224
- index = readUntil(text, index + 1, ATTRIBUTE_NAME_END);
10272
+ index = readWhile(text, index + 1, ATTRIBUTE_NAME);
10225
10273
  const name = text.slice(attributeStart, index);
10226
10274
  if (offset <= index) return {
10227
10275
  context: {
@@ -10241,7 +10289,7 @@ function readTag(text, start, offset, open) {
10241
10289
  if (quote === "\"" || quote === "'") {
10242
10290
  const close = text.indexOf(quote, valueStart + 1);
10243
10291
  if (close < 0 || offset <= close) return {
10244
- context: valueContext(text.slice(valueStart + 1, offset), valueStart + 1, element, name, open),
10292
+ context: valueContext(text.slice(valueStart + 1, offset), valueStart + 1, element, name, openElements),
10245
10293
  end: offset
10246
10294
  };
10247
10295
  attributes[name] = text.slice(valueStart + 1, close);
@@ -10253,7 +10301,8 @@ function readTag(text, start, offset, open) {
10253
10301
  kind: "attributeValue",
10254
10302
  start: valueStart,
10255
10303
  element,
10256
- attribute: name
10304
+ attribute: name,
10305
+ loopVariables: loopVariables(openElements)
10257
10306
  },
10258
10307
  end: valueEnd
10259
10308
  };
@@ -10262,32 +10311,34 @@ function readTag(text, start, offset, open) {
10262
10311
  }
10263
10312
  }
10264
10313
  }
10265
- function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10314
+ function valueContext(valueBeforeCursor, valueStart, element, attribute, openElements) {
10315
+ const variables = loopVariables(openElements);
10266
10316
  const bindingStart = valueBeforeCursor.lastIndexOf("{{");
10267
10317
  if (bindingStart >= 0 && !valueBeforeCursor.includes("}}", bindingStart)) return {
10268
10318
  kind: "binding",
10269
10319
  start: valueStart + bindingStart + 2,
10270
- loopVariables: loopVariables(open)
10320
+ loopVariables: variables
10271
10321
  };
10272
10322
  return {
10273
10323
  kind: "attributeValue",
10274
10324
  start: valueStart,
10275
10325
  element,
10276
- attribute
10326
+ attribute,
10327
+ loopVariables: variables
10277
10328
  };
10278
10329
  }
10279
- function closeElement(open, tag) {
10280
- for (let index = open.length - 1; index >= 0; index--) if (open[index].tag === tag) {
10281
- open.length = index;
10330
+ function closeElement(openElements, tag) {
10331
+ for (let index = openElements.length - 1; index >= 0; index--) if (openElements[index].tag === tag) {
10332
+ openElements.length = index;
10282
10333
  return;
10283
10334
  }
10284
10335
  }
10285
- function loopVariables(open) {
10336
+ function loopVariables(openElements) {
10286
10337
  const variables = [];
10287
- for (const element of open) {
10338
+ for (const element of openElements) {
10288
10339
  if (element.tag !== "For") continue;
10289
10340
  const { item = "item", index = "index", each = "" } = element.attributes;
10290
- const list = each.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10341
+ const list = bindingSource(each);
10291
10342
  variables.push({
10292
10343
  name: item,
10293
10344
  role: "item",
@@ -10300,6 +10351,9 @@ function loopVariables(open) {
10300
10351
  }
10301
10352
  return variables;
10302
10353
  }
10354
+ function bindingSource(value) {
10355
+ return value.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10356
+ }
10303
10357
  function readWhile(text, index, pattern) {
10304
10358
  while (index < text.length && pattern.test(text[index])) index++;
10305
10359
  return index;
@@ -10308,7 +10362,6 @@ function readUntil(text, index, pattern) {
10308
10362
  while (index < text.length && !pattern.test(text[index])) index++;
10309
10363
  return index;
10310
10364
  }
10311
- /** The start and end of the run of `pattern` characters around `offset`. */
10312
10365
  function wordAt(text, offset, pattern) {
10313
10366
  let start = offset;
10314
10367
  while (start > 0 && pattern.test(text[start - 1])) start--;
@@ -10318,92 +10371,163 @@ function wordAt(text, offset, pattern) {
10318
10371
  };
10319
10372
  }
10320
10373
  //#endregion
10321
- //#region src/lsp/features.ts
10322
- const TAG_NAME = /[A-Za-z0-9_.:-]/;
10323
- const ATTRIBUTE_NAME = /[^\s=/>"']/;
10374
+ //#region src/lsp/shapes.ts
10375
+ const NUMBER_SHAPE = {
10376
+ kind: "value",
10377
+ type: "number"
10378
+ };
10379
+ const MEMBER_CHAIN = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\.\s*$/;
10380
+ function parseBinding(source) {
10381
+ try {
10382
+ return parseExpression(source);
10383
+ } catch (error) {
10384
+ if (error instanceof ScriptParseError) return;
10385
+ throw error;
10386
+ }
10387
+ }
10388
+ function shapeOf(expression, scope) {
10389
+ switch (expression.type) {
10390
+ case "Identifier":
10391
+ if (scope.loops.has(expression.name)) return scope.loops.get(expression.name);
10392
+ return scope.state[expression.name];
10393
+ case "Member": {
10394
+ const owner = shapeOf(expression.object, scope);
10395
+ if (!owner) return;
10396
+ const { property } = expression;
10397
+ if (property.type === "Literal" && typeof property.value === "string") return membersOf(owner)[property.value];
10398
+ if (owner.kind === "list") return owner.item;
10399
+ return;
10400
+ }
10401
+ default: return;
10402
+ }
10403
+ }
10404
+ function membersOf(shape) {
10405
+ switch (shape.kind) {
10406
+ case "object": return shape.properties;
10407
+ case "list": return { length: NUMBER_SHAPE };
10408
+ case "value": return {};
10409
+ }
10410
+ }
10411
+ function describeMembers(shape) {
10412
+ const names = Object.keys(membersOf(shape));
10413
+ if (names.length === 0) return "none";
10414
+ return names.join(", ");
10415
+ }
10416
+ function loopScope(variables, state) {
10417
+ const loops = /* @__PURE__ */ new Map();
10418
+ for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER_SHAPE);
10419
+ else loops.set(variable.name, itemShape(variable.list, {
10420
+ state,
10421
+ loops
10422
+ }));
10423
+ return {
10424
+ state,
10425
+ loops
10426
+ };
10427
+ }
10428
+ function bindingScope(loopVariables, page) {
10429
+ const shapes = page.state?.shapes;
10430
+ if (!shapes) return;
10431
+ return loopScope(loopVariables, shapes);
10432
+ }
10433
+ function itemShape(each, scope) {
10434
+ const list = parseBinding(bindingSource(each));
10435
+ if (!list) return;
10436
+ const shape = shapeOf(list, scope);
10437
+ if (shape?.kind === "list") return shape.item;
10438
+ }
10439
+ function withLoop(scope, item, itemOfList, index) {
10440
+ const loops = new Map(scope.loops);
10441
+ loops.set(item, itemOfList);
10442
+ loops.set(index, NUMBER_SHAPE);
10443
+ return {
10444
+ state: scope.state,
10445
+ loops
10446
+ };
10447
+ }
10448
+ function isMember(text, bindingStart, wordStart) {
10449
+ return text.slice(bindingStart, wordStart).trimEnd().endsWith(".");
10450
+ }
10451
+ function ownerOf(text, bindingStart, wordStart, scope) {
10452
+ const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, wordStart))?.[1];
10453
+ if (chain === void 0) return;
10454
+ const expression = parseBinding(chain);
10455
+ if (!expression || !scope) return;
10456
+ return shapeOf(expression, scope);
10457
+ }
10458
+ //#endregion
10459
+ //#region src/lsp/completion.ts
10324
10460
  const IMAGE_ATTRIBUTES = ["src", "image"];
10325
- const IDENTIFIER = /[\w$]/;
10326
- const VALUE = /[^\s"']/;
10461
+ const IDENTIFIER_BEFORE_CURSOR = /[A-Za-z_$][\w$]*$/;
10327
10462
  const CONTROL_FLOW_SNIPPETS = {
10328
10463
  If: "If condition=\"{{ $1 }}\">$0</If>",
10329
10464
  ElseIf: "ElseIf condition=\"{{ $1 }}\">$0</ElseIf>",
10330
10465
  Else: "Else>$0</Else>",
10331
10466
  For: "For each=\"{{ $1 }}\" key=\"$2\">$0</For>"
10332
10467
  };
10333
- const CONTROL_FLOW_DOCS = {
10334
- If: "Shows its children when `condition` is true.",
10335
- ElseIf: "Follows an `<If>` or `<ElseIf>`: shows its children when the ones before are false and `condition` is true.",
10336
- Else: "Follows an `<If>` or `<ElseIf>`: shows its children when the ones before are false.",
10337
- For: "Repeats its children for each item of `each`, as `item` and `index`. `key` is a property of each item, `*this` for the item itself, or `*index` to match items by position."
10338
- };
10339
- const CONTROL_FLOW_ATTRIBUTE_DOCS = {
10340
- condition: "A single `{{ }}` binding",
10341
- each: "The list to repeat, as a single `{{ }}` binding",
10342
- item: "The name of each item (default `item`)",
10343
- index: "The name of each position (default `index`)",
10344
- key: "A property of each item, `*this` or `*index`"
10345
- };
10346
- function complete(document, offset, page, { components }) {
10468
+ function complete(document, offset, page, components) {
10347
10469
  const text = document.getText();
10348
10470
  const context = markupContextAt(text, offset);
10349
- const range = (start) => ({
10350
- start: document.positionAt(start),
10351
- end: document.positionAt(offset)
10352
- });
10353
10471
  switch (context.kind) {
10354
- case "tagName": return tagCompletions(context.parents, components).map((item) => ({
10355
- ...item,
10356
- textEdit: {
10357
- range: range(context.start),
10358
- newText: item.insertText ?? item.label
10359
- }
10360
- }));
10472
+ case "tagName": {
10473
+ const items = tagCompletions(context.parents, components);
10474
+ return replacingFrom(document, context.start, offset, items);
10475
+ }
10361
10476
  case "closeTag":
10362
10477
  if (!context.open) return [];
10363
10478
  return [{
10364
10479
  label: `/${context.open.tag}`,
10365
10480
  kind: import_main.CompletionItemKind.Property,
10366
10481
  textEdit: {
10367
- range: range(context.start),
10482
+ range: rangeFrom(document, context.start, offset),
10368
10483
  newText: `${context.open.tag}>`
10369
10484
  }
10370
10485
  }];
10371
- case "attributeName": return attributeCompletions(context.element, components).map((item) => ({
10372
- ...item,
10373
- textEdit: {
10374
- range: range(context.start),
10375
- newText: item.insertText ?? item.label
10376
- }
10377
- }));
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
- }));
10486
+ case "attributeName": {
10487
+ const items = attributeCompletions(context.element, components);
10488
+ return replacingFrom(document, context.start, offset, items);
10489
+ }
10490
+ case "attributeValue": {
10491
+ const scope = bindingScope(context.loopVariables, page);
10492
+ const items = valueCompletions(context.element, context.attribute, page, scope, components);
10493
+ return replacingFrom(document, context.start, offset, items);
10494
+ }
10385
10495
  case "binding": {
10386
10496
  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) => ({
10390
- ...item,
10391
- textEdit: {
10392
- range: range(offset - identifier.length),
10393
- newText: item.label
10394
- }
10395
- }));
10497
+ const wordStart = offset - (IDENTIFIER_BEFORE_CURSOR.exec(typed)?.[0] ?? "").length;
10498
+ const scope = bindingScope(context.loopVariables, page);
10499
+ let items;
10500
+ if (isMember(text, context.start, wordStart)) items = memberCompletions(ownerOf(text, context.start, wordStart, scope));
10501
+ else items = bindingCompletions(context.loopVariables, page, scope);
10502
+ return replacingFrom(document, wordStart, offset, items);
10396
10503
  }
10397
10504
  default: return [];
10398
10505
  }
10399
10506
  }
10507
+ function rangeFrom(document, start, offset) {
10508
+ return {
10509
+ start: document.positionAt(start),
10510
+ end: document.positionAt(offset)
10511
+ };
10512
+ }
10513
+ function replacingFrom(document, start, offset, items) {
10514
+ return items.map((item) => ({
10515
+ ...item,
10516
+ textEdit: {
10517
+ range: rangeFrom(document, start, offset),
10518
+ newText: item.insertText ?? item.label
10519
+ }
10520
+ }));
10521
+ }
10400
10522
  function tagCompletions(parents, components) {
10401
10523
  if (parents.length === 0) {
10402
10524
  const root = components[ROOT_TAG];
10403
- return root ? [componentItem(root, "0")] : [];
10525
+ if (!root) return [];
10526
+ return [componentItem(root, "0")];
10404
10527
  }
10405
10528
  const parent = [...parents].reverse().find((element) => !(element.tag in CONTROL_FLOW_TAGS));
10406
- const parentManifest = parent && components[parent.tag];
10529
+ let parentManifest;
10530
+ if (parent) parentManifest = components[parent.tag];
10407
10531
  const items = Object.values(components).filter((component) => !parentManifest || canHaveChildren(parentManifest, component)).map((component) => componentItem(component, "1"));
10408
10532
  for (const [tag, snippet] of Object.entries(CONTROL_FLOW_SNIPPETS)) items.push({
10409
10533
  label: tag,
@@ -10420,7 +10544,8 @@ function tagCompletions(parents, components) {
10420
10544
  return items;
10421
10545
  }
10422
10546
  function componentItem(component, sortGroup) {
10423
- const snippet = component.children === "none" ? `${component.name}$1 />` : `${component.name}$1>$0</${component.name}>`;
10547
+ let snippet = `${component.name}$1>$0</${component.name}>`;
10548
+ if (component.children === "none") snippet = `${component.name}$1 />`;
10424
10549
  return {
10425
10550
  label: component.name,
10426
10551
  kind: import_main.CompletionItemKind.Class,
@@ -10435,18 +10560,18 @@ function componentItem(component, sortGroup) {
10435
10560
  };
10436
10561
  }
10437
10562
  function attributeCompletions(element, components) {
10438
- const written = (name) => name in element.attributes;
10563
+ const isWritten = (name) => name in element.attributes;
10439
10564
  const controlFlow = CONTROL_FLOW_TAGS[element.tag];
10440
- if (controlFlow) return controlFlow.attributes.filter((name) => !written(name)).map((name) => ({
10565
+ if (controlFlow) return controlFlow.attributes.filter((name) => !isWritten(name)).map((name) => ({
10441
10566
  label: name,
10442
10567
  kind: import_main.CompletionItemKind.Property,
10443
10568
  detail: CONTROL_FLOW_ATTRIBUTE_DOCS[name] ?? name,
10444
- insertText: name === "condition" || name === "each" ? `${name}="{{ $1 }}"` : `${name}="$1"`,
10569
+ insertText: controlFlowAttributeSnippet(name),
10445
10570
  insertTextFormat: import_main.InsertTextFormat.Snippet
10446
10571
  }));
10447
10572
  const component = components[element.tag];
10448
10573
  if (!component) return [];
10449
- const props = Object.entries(component.props).filter(([name]) => !written(name)).map(([name, type]) => ({
10574
+ const props = Object.entries(component.props).filter(([name]) => !isWritten(name)).map(([name, type]) => ({
10450
10575
  label: name,
10451
10576
  kind: import_main.CompletionItemKind.Property,
10452
10577
  detail: describeType(type),
@@ -10454,7 +10579,7 @@ function attributeCompletions(element, components) {
10454
10579
  insertTextFormat: import_main.InsertTextFormat.Snippet,
10455
10580
  sortText: `0${name}`
10456
10581
  }));
10457
- const events = component.events.map((event) => eventProp(event)).filter((name) => !written(name)).map((name) => ({
10582
+ const events = component.events.map((event) => eventProp(event)).filter((name) => !isWritten(name)).map((name) => ({
10458
10583
  label: name,
10459
10584
  kind: import_main.CompletionItemKind.Event,
10460
10585
  detail: "a mutation or action in controller.ts",
@@ -10464,20 +10589,20 @@ function attributeCompletions(element, components) {
10464
10589
  }));
10465
10590
  return [...props, ...events];
10466
10591
  }
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
- }];
10592
+ function controlFlowAttributeSnippet(name) {
10593
+ if (name === "condition" || name === "each") return `${name}="{{ $1 }}"`;
10594
+ return `${name}="$1"`;
10595
+ }
10596
+ function valueCompletions(element, attribute, page, scope, components) {
10597
+ if (element.tag === "For" && attribute === "key") return forKeyCompletions(element, scope);
10477
10598
  const component = components[element.tag];
10478
10599
  if (!component) return [];
10479
- if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerItems(page);
10600
+ if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerCompletions(page);
10480
10601
  const type = component.props[attribute];
10602
+ if (type && isWordsOr(type)) return type.words.map((value) => ({
10603
+ label: value,
10604
+ kind: import_main.CompletionItemKind.EnumMember
10605
+ }));
10481
10606
  if (Array.isArray(type)) return type.map((value) => ({
10482
10607
  label: value,
10483
10608
  kind: import_main.CompletionItemKind.EnumMember
@@ -10492,36 +10617,394 @@ function valueCompletions(element, attribute, page, components) {
10492
10617
  }));
10493
10618
  return [];
10494
10619
  }
10495
- function handlerItems(page) {
10620
+ function forKeyCompletions(element, scope) {
10621
+ let properties = [];
10622
+ if (scope) {
10623
+ const item = itemShape(element.attributes.each ?? "", scope);
10624
+ if (item?.kind === "object") properties = memberCompletions(item);
10625
+ }
10626
+ return [
10627
+ ...properties,
10628
+ {
10629
+ label: "*this",
10630
+ kind: import_main.CompletionItemKind.Value,
10631
+ detail: "the item itself"
10632
+ },
10633
+ {
10634
+ label: "*index",
10635
+ kind: import_main.CompletionItemKind.Value,
10636
+ detail: "match items by position"
10637
+ }
10638
+ ];
10639
+ }
10640
+ function handlerCompletions(page) {
10496
10641
  const { controller } = page;
10497
10642
  if (!controller) return [];
10498
- return [...(controller.mutations ?? []).map((name) => ({
10643
+ const mutations = (controller.mutations ?? []).map((name) => ({
10499
10644
  label: name,
10500
10645
  kind: import_main.CompletionItemKind.Method,
10501
10646
  detail: "mutation"
10502
- })), ...(controller.actions ?? []).map((name) => ({
10647
+ }));
10648
+ const actions = (controller.actions ?? []).map((name) => ({
10503
10649
  label: name,
10504
10650
  kind: import_main.CompletionItemKind.Function,
10505
10651
  detail: "action"
10506
- }))];
10652
+ }));
10653
+ return [...mutations, ...actions];
10507
10654
  }
10508
- function bindingCompletions(loopVariables, page) {
10655
+ function bindingCompletions(loopVariables, page, scope) {
10509
10656
  const items = /* @__PURE__ */ new Map();
10510
- for (const key of page.state?.keys ?? []) items.set(key, {
10511
- label: key,
10512
- kind: import_main.CompletionItemKind.Field,
10513
- detail: "state",
10514
- sortText: `1${key}`
10515
- });
10657
+ for (const key of page.state?.keys ?? []) {
10658
+ const shape = scope?.state[key];
10659
+ items.set(key, {
10660
+ label: key,
10661
+ kind: import_main.CompletionItemKind.Field,
10662
+ detail: shape ? `state: ${shape.type}` : "state",
10663
+ sortText: `1${key}`
10664
+ });
10665
+ }
10516
10666
  for (const variable of loopVariables) items.set(variable.name, {
10517
10667
  label: variable.name,
10518
10668
  kind: import_main.CompletionItemKind.Variable,
10519
- detail: describeLoopVariable(variable),
10669
+ detail: describeLoopVariable(variable, scope?.loops.get(variable.name)),
10520
10670
  sortText: `0${variable.name}`
10521
10671
  });
10522
10672
  return [...items.values()];
10523
10673
  }
10524
- function hover(document, offset, page, { components }) {
10674
+ function memberCompletions(owner) {
10675
+ if (!owner) return [];
10676
+ return Object.entries(membersOf(owner)).map(([name, shape]) => ({
10677
+ label: name,
10678
+ kind: import_main.CompletionItemKind.Field,
10679
+ detail: shape.type
10680
+ }));
10681
+ }
10682
+ //#endregion
10683
+ //#region src/lsp/check-members.ts
10684
+ const BINDING = /{{([\s\S]*?)}}/g;
10685
+ function checkMembers(text, state) {
10686
+ let nodes;
10687
+ try {
10688
+ nodes = parseDmlToIR(text, {
10689
+ locations: true,
10690
+ whitespace: "preserve"
10691
+ });
10692
+ } catch {
10693
+ return [];
10694
+ }
10695
+ const problems = [];
10696
+ checkNodes(text, nodes, {
10697
+ state,
10698
+ loops: /* @__PURE__ */ new Map()
10699
+ }, problems);
10700
+ return problems;
10701
+ }
10702
+ function checkNodes(text, nodes, scope, problems) {
10703
+ for (const node of nodes) {
10704
+ const from = node.loc?.offset ?? 0;
10705
+ if (node.type === "Text") {
10706
+ checkBindings(text, node.content, from, scope, problems);
10707
+ continue;
10708
+ }
10709
+ for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10710
+ if (node.tag !== "For") {
10711
+ checkNodes(text, node.children, scope, problems);
10712
+ continue;
10713
+ }
10714
+ const { each = "", item = "item", index = "index", key } = node.attributes;
10715
+ const itemOfList = itemShape(each, scope);
10716
+ if (key !== void 0 && !key.startsWith("*") && itemOfList?.kind === "object" && !(key in itemOfList.properties)) problems.push({
10717
+ message: `The <For> \`key\` "${key}" is not a property of \`${itemOfList.type}\`. It has: ${describeMembers(itemOfList)}`,
10718
+ offset: offsetOf(text, `key="${key}"`, from)
10719
+ });
10720
+ checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10721
+ }
10722
+ }
10723
+ function checkBindings(text, value, from, scope, problems) {
10724
+ for (const [binding, source = ""] of value.matchAll(BINDING)) {
10725
+ const expression = parseBinding(source);
10726
+ if (!expression) continue;
10727
+ const bindingOffset = offsetOf(text, binding, from);
10728
+ for (const { name, owner } of unknownMembers(expression, scope)) problems.push({
10729
+ message: `\`${name}\` is not a property of \`${owner.type}\`. It has: ${describeMembers(owner)}`,
10730
+ offset: memberOffset(bindingOffset, binding, name)
10731
+ });
10732
+ }
10733
+ }
10734
+ function unknownMembers(expression, scope) {
10735
+ const unknownIn = (node) => unknownMembers(node, scope);
10736
+ switch (expression.type) {
10737
+ case "Literal":
10738
+ case "Identifier": return [];
10739
+ case "Member": {
10740
+ const inner = [...unknownIn(expression.object), ...unknownIn(expression.property)];
10741
+ const { property } = expression;
10742
+ if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10743
+ const owner = shapeOf(expression.object, scope);
10744
+ if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10745
+ name: property.value,
10746
+ owner
10747
+ }];
10748
+ return inner;
10749
+ }
10750
+ case "Unary": return unknownIn(expression.argument);
10751
+ case "Binary": return [...unknownIn(expression.left), ...unknownIn(expression.right)];
10752
+ case "Conditional": return [
10753
+ ...unknownIn(expression.test),
10754
+ ...unknownIn(expression.consequent),
10755
+ ...unknownIn(expression.alternate)
10756
+ ];
10757
+ }
10758
+ }
10759
+ function offsetOf(text, written, from) {
10760
+ const foundAt = text.indexOf(written, from);
10761
+ if (foundAt < 0) return from;
10762
+ return foundAt;
10763
+ }
10764
+ function memberOffset(bindingOffset, binding, name) {
10765
+ const escapedName = name.replaceAll("$", "\\$");
10766
+ const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10767
+ if (!match) return bindingOffset;
10768
+ return bindingOffset + match.index;
10769
+ }
10770
+ //#endregion
10771
+ //#region src/lsp/diagnostics.ts
10772
+ function diagnose(document, file, page, components) {
10773
+ const diagnostics = [];
10774
+ const context = {
10775
+ file,
10776
+ components,
10777
+ onWarning: (warning) => {
10778
+ diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning));
10779
+ }
10780
+ };
10781
+ if (page.statics) context.statics = page.statics;
10782
+ if (page.state?.keys) context.stateKeys = page.state.keys;
10783
+ const mutations = page.controller?.mutations;
10784
+ const actions = page.controller?.actions;
10785
+ if (mutations && actions) context.handlers = [...mutations, ...actions];
10786
+ try {
10787
+ compileView(document.getText(), context);
10788
+ } catch (error) {
10789
+ if (!(error instanceof SourceError)) throw error;
10790
+ diagnostics.push(sourceDiagnostic(document, error, import_main.DiagnosticSeverity.Error));
10791
+ }
10792
+ 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));
10793
+ return diagnostics;
10794
+ }
10795
+ function sourceDiagnostic(document, problem, severity) {
10796
+ return diagnosticAt(document, problem.loc?.offset ?? 0, problem.reason, severity);
10797
+ }
10798
+ function diagnosticAt(document, start, message, severity) {
10799
+ const text = document.getText();
10800
+ const end = readUntil(text, start, /[\s>]/);
10801
+ const atLeastOneCharacter = Math.min(start + 1, text.length);
10802
+ return {
10803
+ severity,
10804
+ source: "dot",
10805
+ message,
10806
+ range: {
10807
+ start: document.positionAt(start),
10808
+ end: document.positionAt(Math.max(end, atLeastOneCharacter))
10809
+ }
10810
+ };
10811
+ }
10812
+ //#endregion
10813
+ //#region src/lsp/page.ts
10814
+ async function readPage(viewFile, readText, stateTypes) {
10815
+ const dir = dirname(viewFile);
10816
+ const root = findDotRoot(dir);
10817
+ const [state, controller, statics] = await Promise.all([
10818
+ readState(join(dir, "state.ts"), root, readText, stateTypes),
10819
+ readController(join(dir, "controller.ts"), readText),
10820
+ root === void 0 ? void 0 : listStatics(root).catch(() => void 0)
10821
+ ]);
10822
+ return {
10823
+ root,
10824
+ state,
10825
+ controller,
10826
+ statics
10827
+ };
10828
+ }
10829
+ async function readState(file, root, readText, stateTypes) {
10830
+ const text = await readText(file);
10831
+ if (text === void 0) return;
10832
+ try {
10833
+ const { keys, keyOffsets } = parseStateModule(text, file);
10834
+ return {
10835
+ file,
10836
+ text,
10837
+ keys,
10838
+ offsets: keyOffsets,
10839
+ shapes: readShapes(stateTypes, root, file, text)
10840
+ };
10841
+ } catch {
10842
+ return;
10843
+ }
10844
+ }
10845
+ function readShapes(stateTypes, root, file, text) {
10846
+ try {
10847
+ return stateTypes.read(root, file, text);
10848
+ } catch {
10849
+ return;
10850
+ }
10851
+ }
10852
+ async function readController(file, readText) {
10853
+ const text = await readText(file);
10854
+ if (text === void 0) return;
10855
+ try {
10856
+ const { mutations, actions, handlerOffsets } = parseLogicModule(text, file);
10857
+ return {
10858
+ file,
10859
+ text,
10860
+ mutations,
10861
+ actions,
10862
+ offsets: handlerOffsets
10863
+ };
10864
+ } catch {
10865
+ return;
10866
+ }
10867
+ }
10868
+ //#endregion
10869
+ //#region src/lsp/state-types.ts
10870
+ const MAX_DEPTH = 8;
10871
+ const FALLBACK_OPTIONS = {
10872
+ target: ts.ScriptTarget.ES2022,
10873
+ module: ts.ModuleKind.ESNext,
10874
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
10875
+ strict: true,
10876
+ noEmit: true,
10877
+ skipLibCheck: true
10878
+ };
10879
+ var StateTypes = class {
10880
+ #projects = /* @__PURE__ */ new Map();
10881
+ read(root, file, text) {
10882
+ const key = root ?? file;
10883
+ let project = this.#projects.get(key);
10884
+ if (!project) {
10885
+ project = new TypeScriptProject(root);
10886
+ this.#projects.set(key, project);
10887
+ }
10888
+ return project.read(file, text);
10889
+ }
10890
+ };
10891
+ var TypeScriptProject = class {
10892
+ #options;
10893
+ #fileNames;
10894
+ #open = /* @__PURE__ */ new Map();
10895
+ #service;
10896
+ constructor(root) {
10897
+ const config = root === void 0 ? void 0 : readConfig(root);
10898
+ this.#options = config?.options ?? FALLBACK_OPTIONS;
10899
+ this.#fileNames = config?.fileNames ?? [];
10900
+ const host = {
10901
+ getCompilationSettings: () => this.#options,
10902
+ getScriptFileNames: () => [.../* @__PURE__ */ new Set([...this.#fileNames, ...this.#open.keys()])],
10903
+ getScriptVersion: (file) => {
10904
+ const open = this.#open.get(file);
10905
+ if (open) return String(open.version);
10906
+ return String(ts.sys.getModifiedTime?.(file)?.getTime() ?? 0);
10907
+ },
10908
+ getScriptSnapshot: (file) => {
10909
+ const text = this.#open.get(file)?.text ?? ts.sys.readFile(file);
10910
+ if (text === void 0) return;
10911
+ return ts.ScriptSnapshot.fromString(text);
10912
+ },
10913
+ getCurrentDirectory: () => root ?? process.cwd(),
10914
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
10915
+ fileExists: (file) => ts.sys.fileExists(file),
10916
+ readFile: (file) => ts.sys.readFile(file),
10917
+ readDirectory: (...args) => ts.sys.readDirectory(...args),
10918
+ directoryExists: (dir) => ts.sys.directoryExists(dir),
10919
+ getDirectories: (dir) => ts.sys.getDirectories(dir),
10920
+ realpath: (path) => ts.sys.realpath?.(path) ?? path
10921
+ };
10922
+ this.#service = ts.createLanguageService(host);
10923
+ }
10924
+ read(file, text) {
10925
+ const open = this.#open.get(file);
10926
+ if (open?.text !== text) this.#open.set(file, {
10927
+ text,
10928
+ version: (open?.version ?? 0) + 1
10929
+ });
10930
+ const program = this.#service.getProgram();
10931
+ const source = program?.getSourceFile(file);
10932
+ if (!program || !source) return;
10933
+ const checker = program.getTypeChecker();
10934
+ const module = checker.getSymbolAtLocation(source);
10935
+ if (!module) return;
10936
+ let exported = checker.tryGetMemberInModuleExports("default", module);
10937
+ if (!exported) return;
10938
+ if (exported.flags & ts.SymbolFlags.Alias) exported = checker.getAliasedSymbol(exported);
10939
+ const state = checker.getTypeOfSymbol(exported);
10940
+ const shapes = {};
10941
+ for (const property of checker.getPropertiesOfType(state)) shapes[property.name] = propertyShape(checker, property, 0, /* @__PURE__ */ new Set());
10942
+ return shapes;
10943
+ }
10944
+ };
10945
+ function readConfig(root) {
10946
+ const file = join(root, "tsconfig.json");
10947
+ if (!existsSync(file)) return;
10948
+ const configFile = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
10949
+ if (configFile.error) return;
10950
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, root, void 0, file);
10951
+ }
10952
+ function propertyShape(checker, property, depth, seen) {
10953
+ const shape = typeShape(checker, checker.getTypeOfSymbol(property), depth, seen);
10954
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
10955
+ if (!declaration) return shape;
10956
+ const source = declaration.getSourceFile();
10957
+ const { line, character } = source.getLineAndCharacterOfPosition(declaration.getStart());
10958
+ return {
10959
+ ...shape,
10960
+ declaration: {
10961
+ file: source.fileName,
10962
+ line,
10963
+ character
10964
+ }
10965
+ };
10966
+ }
10967
+ function typeShape(checker, type, depth, seen) {
10968
+ const present = checker.getNonNullableType(type);
10969
+ const name = checker.typeToString(present);
10970
+ if (depth >= MAX_DEPTH || seen.has(present) || present.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return {
10971
+ kind: "value",
10972
+ type: name
10973
+ };
10974
+ const inner = new Set(seen).add(present);
10975
+ if (checker.isArrayType(present) || checker.isTupleType(present)) {
10976
+ const item = checker.getIndexTypeOfType(present, ts.IndexKind.Number);
10977
+ let itemShape = {
10978
+ kind: "value",
10979
+ type: "unknown"
10980
+ };
10981
+ if (item) itemShape = typeShape(checker, item, depth + 1, inner);
10982
+ return {
10983
+ kind: "list",
10984
+ type: name,
10985
+ item: itemShape
10986
+ };
10987
+ }
10988
+ if (!isRecord(checker, present)) return {
10989
+ kind: "value",
10990
+ type: name
10991
+ };
10992
+ const properties = {};
10993
+ for (const property of checker.getPropertiesOfType(present)) properties[property.name] = propertyShape(checker, property, depth + 1, inner);
10994
+ return {
10995
+ kind: "object",
10996
+ type: name,
10997
+ properties
10998
+ };
10999
+ }
11000
+ function isRecord(checker, type) {
11001
+ return (type.isUnion() ? type.types : [type]).every((member) => (member.flags & ts.TypeFlags.Object) !== 0 && member.getCallSignatures().length === 0 && checker.getIndexInfosOfType(member).length === 0);
11002
+ }
11003
+ //#endregion
11004
+ //#region src/lsp/symbols.ts
11005
+ const IDENTIFIER = /[\w$]/;
11006
+ const VALUE = /[^\s"']/;
11007
+ function hover(document, offset, page, components) {
10525
11008
  const found = symbolAt(document.getText(), offset, page);
10526
11009
  if (!found) return null;
10527
11010
  const markdown = describeSymbol(found.symbol, page, components);
@@ -10541,8 +11024,22 @@ function definition(document, offset, page) {
10541
11024
  const found = symbolAt(document.getText(), offset, page);
10542
11025
  if (!found) return null;
10543
11026
  const { symbol } = found;
10544
- if (symbol.kind === "handler" && page.controller) return locationIn(page.controller.file, page.controller.text, page.controller.offsets[symbol.name]);
10545
- if (symbol.kind === "state" && page.state) return locationIn(page.state.file, page.state.text, page.state.offsets[symbol.name]);
11027
+ if (symbol.kind === "handler" && page.controller) return definitionIn(page.controller, symbol.name);
11028
+ if (symbol.kind === "state" && page.state) return definitionIn(page.state, symbol.name);
11029
+ if (symbol.kind === "property" && symbol.shape.declaration) {
11030
+ const { file, line, character } = symbol.shape.declaration;
11031
+ const position = {
11032
+ line,
11033
+ character
11034
+ };
11035
+ return {
11036
+ uri: pathToFileURL(file).href,
11037
+ range: {
11038
+ start: position,
11039
+ end: position
11040
+ }
11041
+ };
11042
+ }
10546
11043
  if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
10547
11044
  return null;
10548
11045
  }
@@ -10564,7 +11061,8 @@ function symbolAt(text, offset, page) {
10564
11061
  case "attributeName": {
10565
11062
  const { start, end } = wordAt(text, offset, ATTRIBUTE_NAME);
10566
11063
  const name = text.slice(start, end);
10567
- return name ? {
11064
+ if (!name) return null;
11065
+ return {
10568
11066
  symbol: {
10569
11067
  kind: "attribute",
10570
11068
  tag: context.element.tag,
@@ -10572,7 +11070,7 @@ function symbolAt(text, offset, page) {
10572
11070
  },
10573
11071
  start,
10574
11072
  end
10575
- } : null;
11073
+ };
10576
11074
  }
10577
11075
  case "attributeValue": {
10578
11076
  const { start, end } = wordAt(text, offset, VALUE);
@@ -10598,12 +11096,30 @@ function symbolAt(text, offset, page) {
10598
11096
  case "binding": {
10599
11097
  const { start, end } = wordAt(text, offset, IDENTIFIER);
10600
11098
  const name = text.slice(start, end);
10601
- if (!name || text.slice(context.start, start).trimEnd().endsWith(".")) return null;
11099
+ if (!name) return null;
11100
+ const scope = bindingScope(context.loopVariables, page);
11101
+ if (isMember(text, context.start, start)) {
11102
+ const owner = ownerOf(text, context.start, start, scope);
11103
+ if (!owner) return null;
11104
+ const shape = membersOf(owner)[name];
11105
+ if (!shape) return null;
11106
+ return {
11107
+ symbol: {
11108
+ kind: "property",
11109
+ name,
11110
+ owner,
11111
+ shape
11112
+ },
11113
+ start,
11114
+ end
11115
+ };
11116
+ }
10602
11117
  const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
10603
11118
  if (variable) return {
10604
11119
  symbol: {
10605
11120
  kind: "loopVariable",
10606
- variable
11121
+ variable,
11122
+ shape: scope?.loops.get(name)
10607
11123
  },
10608
11124
  start,
10609
11125
  end
@@ -10623,56 +11139,45 @@ function symbolAt(text, offset, page) {
10623
11139
  function describeSymbol(symbol, page, components) {
10624
11140
  switch (symbol.kind) {
10625
11141
  case "component": {
10626
- const controlFlow = CONTROL_FLOW_DOCS[symbol.tag];
10627
- if (controlFlow) return `**<${symbol.tag}>** · control flow\n\n${controlFlow}`;
10628
- const component = components[symbol.tag];
10629
- return component ? describeComponent(component) : null;
10630
- }
10631
- case "attribute": {
10632
- if (CONTROL_FLOW_TAGS[symbol.tag]) {
10633
- const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[symbol.name];
10634
- return doc ? `\`${symbol.name}\`: ${doc}` : null;
10635
- }
11142
+ const controlFlowDoc = CONTROL_FLOW_DOCS[symbol.tag];
11143
+ if (controlFlowDoc) return `**<${symbol.tag}>** · control flow\n\n${controlFlowDoc}`;
10636
11144
  const component = components[symbol.tag];
10637
11145
  if (!component) return null;
10638
- const type = component.props[symbol.name];
10639
- if (type) return `\`${symbol.name}\`: ${describeType(type)}`;
10640
- if (symbol.name.startsWith(DATA_PREFIX)) return `\`${symbol.name}\`: reaches this element's handlers as \`event.data.${dataKey(symbol.name)}\``;
10641
- if (isEventProp(symbol.name) && component.events.includes(eventName(symbol.name))) return `\`${symbol.name}\`: runs a mutation or action in controller.ts on \`${eventName(symbol.name)}\``;
10642
- return null;
11146
+ return describeComponent(component);
10643
11147
  }
11148
+ case "attribute": return describeAttribute(symbol.tag, symbol.name, components);
10644
11149
  case "handler": {
10645
11150
  const { controller } = page;
10646
11151
  if (controller?.mutations?.includes(symbol.name)) return `mutation \`${symbol.name}\` in controller.ts`;
10647
11152
  if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
10648
11153
  return null;
10649
11154
  }
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)}`;
11155
+ case "state": {
11156
+ if (!page.state?.keys?.includes(symbol.name)) return null;
11157
+ const shape = page.state.shapes?.[symbol.name];
11158
+ return `state \`${symbol.name}\` in state.ts${typeSuffix(shape)}`;
11159
+ }
11160
+ case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
11161
+ case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
10652
11162
  case "static": return `image \`${symbol.path}\``;
10653
11163
  }
10654
11164
  }
10655
- function describeComponent(component) {
10656
- const lines = [`**<${component.name}>** · ${describeChildren(component)}`];
10657
- const props = Object.entries(component.props);
10658
- if (props.length > 0) lines.push("", ...props.map(([name, type]) => `- \`${name}\`: ${describeType(type)}`));
10659
- if (component.events.length > 0) lines.push("", `Events: ${component.events.map((event) => `\`${eventProp(event)}\``).join(", ")}`);
10660
- return lines.join("\n");
10661
- }
10662
- function describeChildren(component) {
10663
- switch (component.children) {
10664
- case "none": return "no children";
10665
- case "text": return "text and inline components";
10666
- case "elements": return "any components";
11165
+ function describeAttribute(tag, name, components) {
11166
+ if (CONTROL_FLOW_TAGS[tag]) {
11167
+ const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[name];
11168
+ if (!doc) return null;
11169
+ return `\`${name}\`: ${doc}`;
10667
11170
  }
11171
+ const component = components[tag];
11172
+ if (!component) return null;
11173
+ const type = component.props[name];
11174
+ if (type) return `\`${name}\`: ${describeType(type)}`;
11175
+ if (name.startsWith(DATA_PREFIX)) return `\`${name}\`: this element's handlers get it as \`event.data.${dataKey(name)}\``;
11176
+ if (isEventProp(name) && component.events.includes(eventName(name))) return `\`${name}\`: on \`${eventName(name)}\`, runs a mutation or action from controller.ts`;
11177
+ return null;
10668
11178
  }
10669
- function describeType(type) {
10670
- if (typeof type === "string") return type;
10671
- return type.map((value) => `"${value}"`).join(" | ");
10672
- }
10673
- function describeLoopVariable(variable) {
10674
- const list = variable.list ? ` \`${variable.list}\`` : "";
10675
- return variable.role === "item" ? `each item of${list}` : `the position in${list}`;
11179
+ function definitionIn(source, name) {
11180
+ return locationIn(source.file, source.text, source.offsets[name]);
10676
11181
  }
10677
11182
  function locationIn(file, text, offset) {
10678
11183
  if (offset === void 0) return null;
@@ -10685,136 +11190,42 @@ function locationIn(file, text, offset) {
10685
11190
  }
10686
11191
  };
10687
11192
  }
10688
- function diagnose(document, file, page, { components }) {
10689
- const diagnostics = [];
10690
- const context = {
10691
- file,
10692
- components,
10693
- onWarning: (warning) => diagnostics.push(diagnosticAt(document, warning, import_main.DiagnosticSeverity.Warning))
10694
- };
10695
- if (page.statics) context.statics = page.statics;
10696
- if (page.state?.keys) context.stateKeys = page.state.keys;
10697
- const { mutations, actions } = page.controller ?? {};
10698
- if (mutations && actions) context.handlers = [...mutations, ...actions];
10699
- try {
10700
- compileView(document.getText(), context);
10701
- } catch (error) {
10702
- if (!(error instanceof SourceError)) throw error;
10703
- diagnostics.push(diagnosticAt(document, error, import_main.DiagnosticSeverity.Error));
10704
- }
10705
- return diagnostics;
10706
- }
10707
- function diagnosticAt(document, problem, severity) {
10708
- const text = document.getText();
10709
- const start = problem.loc?.offset ?? 0;
10710
- let end = start;
10711
- while (end < text.length && !/[\s>]/.test(text[end])) end++;
10712
- return {
10713
- severity,
10714
- source: "dot",
10715
- message: problem.reason,
10716
- range: {
10717
- start: document.positionAt(start),
10718
- end: document.positionAt(Math.max(end, Math.min(start + 1, text.length)))
10719
- }
10720
- };
10721
- }
10722
- //#endregion
10723
- //#region src/lsp/page.ts
10724
- async function readPage(viewFile, readText) {
10725
- const dir = dirname(viewFile);
10726
- const root = findRoot(dir);
10727
- const [state, controller, statics] = await Promise.all([
10728
- readState(join(dir, "state.ts"), readText),
10729
- readController(join(dir, "controller.ts"), readText),
10730
- root === void 0 ? void 0 : listStatics(root).catch(() => void 0)
10731
- ]);
10732
- return {
10733
- root,
10734
- state,
10735
- controller,
10736
- statics
10737
- };
10738
- }
10739
- async function readState(file, readText) {
10740
- const text = await readText(file);
10741
- if (text === void 0) return;
10742
- try {
10743
- const { keys, keyOffsets } = parseStateModule(text, file);
10744
- return {
10745
- file,
10746
- text,
10747
- keys,
10748
- offsets: keyOffsets
10749
- };
10750
- } catch {
10751
- return;
10752
- }
10753
- }
10754
- async function readController(file, readText) {
10755
- const text = await readText(file);
10756
- if (text === void 0) return;
10757
- try {
10758
- const { mutations, actions, handlerOffsets } = parseLogicModule(text, file);
10759
- return {
10760
- file,
10761
- text,
10762
- mutations,
10763
- actions,
10764
- offsets: handlerOffsets
10765
- };
10766
- } catch {
10767
- return;
10768
- }
10769
- }
10770
- function findRoot(dir) {
10771
- while (true) {
10772
- if (existsSync(join(dir, "dot.json"))) return dir;
10773
- const parent = dirname(dir);
10774
- if (parent === dir) return;
10775
- dir = parent;
10776
- }
10777
- }
10778
11193
  //#endregion
10779
11194
  //#region src/lsp/server.ts
10780
- const WATCHED = [
10781
- "**/state.ts",
10782
- "**/controller.ts",
11195
+ const WATCHED_FILES = [
11196
+ "**/*.ts",
10783
11197
  "**/dot.json",
10784
11198
  "**/statics/**"
10785
11199
  ];
10786
- /**
10787
- * Serves .dml files over the Language Server Protocol: completion, hover, go to definition and
10788
- * the compiler's own errors and warnings. It runs from the dot's own installed `dotctl`, so what
10789
- * it knows always matches the Dot version the dot builds with.
10790
- */
10791
11200
  function startLanguageServer(connection) {
10792
11201
  const documents = new import_main.TextDocuments(TextDocument);
10793
- const features = { components: builtinRegistry };
11202
+ const components = builtinRegistry;
11203
+ const stateTypes = new StateTypes();
10794
11204
  let canWatchFiles = false;
10795
11205
  const readText = async (file) => {
10796
- const open = documents.get(pathToFileURL(file).href);
10797
- if (open) return open.getText();
10798
- return existsSync(file) ? readFile(file, "utf8") : void 0;
11206
+ const openDocument = documents.get(pathToFileURL(file).href);
11207
+ if (openDocument) return openDocument.getText();
11208
+ if (!existsSync(file)) return;
11209
+ return readFile(file, "utf8");
10799
11210
  };
10800
- const pageOf = async (uri) => {
11211
+ const openView = async (uri) => {
10801
11212
  const document = documents.get(uri);
10802
11213
  if (!document || !uri.endsWith(".dml")) return;
10803
11214
  const file = fileURLToPath(uri);
10804
11215
  return {
10805
11216
  document,
10806
- page: await readPage(file, readText),
10807
- file
11217
+ file,
11218
+ page: await readPage(file, readText, stateTypes)
10808
11219
  };
10809
11220
  };
10810
11221
  const validate = async (uri) => {
10811
11222
  try {
10812
- const found = await pageOf(uri);
10813
- if (!found) return;
10814
- const diagnostics = diagnose(found.document, found.file, found.page, features);
11223
+ const view = await openView(uri);
11224
+ if (!view) return;
11225
+ const diagnostics = diagnose(view.document, view.file, view.page, components);
10815
11226
  await connection.sendDiagnostics({
10816
11227
  uri,
10817
- version: found.document.version,
11228
+ version: view.document.version,
10818
11229
  diagnostics
10819
11230
  });
10820
11231
  } catch (error) {
@@ -10843,10 +11254,14 @@ function startLanguageServer(connection) {
10843
11254
  };
10844
11255
  });
10845
11256
  connection.onInitialized(() => {
10846
- if (canWatchFiles) connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers: WATCHED.map((globPattern) => ({ globPattern })) });
11257
+ if (canWatchFiles) {
11258
+ const watchers = WATCHED_FILES.map((globPattern) => ({ globPattern }));
11259
+ connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers });
11260
+ }
10847
11261
  });
10848
11262
  documents.onDidChangeContent(({ document }) => {
10849
- document.uri.endsWith(".dml") ? validate(document.uri) : validateAll();
11263
+ if (document.uri.endsWith(".dml")) validate(document.uri);
11264
+ else validateAll();
10850
11265
  });
10851
11266
  documents.onDidClose(({ document }) => {
10852
11267
  connection.sendDiagnostics({
@@ -10857,30 +11272,37 @@ function startLanguageServer(connection) {
10857
11272
  connection.onDidChangeWatchedFiles(() => {
10858
11273
  validateAll();
10859
11274
  });
10860
- connection.onCompletion(async ({ textDocument, position }) => {
10861
- const found = await pageOf(textDocument.uri);
10862
- if (!found) return [];
10863
- return complete(found.document, found.document.offsetAt(position), found.page, features);
11275
+ const openViewAt = async ({ textDocument, position }) => {
11276
+ const view = await openView(textDocument.uri);
11277
+ if (!view) return;
11278
+ return {
11279
+ ...view,
11280
+ offset: view.document.offsetAt(position)
11281
+ };
11282
+ };
11283
+ connection.onCompletion(async (request) => {
11284
+ const view = await openViewAt(request);
11285
+ if (!view) return [];
11286
+ return complete(view.document, view.offset, view.page, components);
10864
11287
  });
10865
- connection.onHover(async ({ textDocument, position }) => {
10866
- const found = await pageOf(textDocument.uri);
10867
- if (!found) return null;
10868
- return hover(found.document, found.document.offsetAt(position), found.page, features);
11288
+ connection.onHover(async (request) => {
11289
+ const view = await openViewAt(request);
11290
+ if (!view) return null;
11291
+ return hover(view.document, view.offset, view.page, components);
10869
11292
  });
10870
- connection.onDefinition(async ({ textDocument, position }) => {
10871
- const found = await pageOf(textDocument.uri);
10872
- if (!found) return null;
10873
- return definition(found.document, found.document.offsetAt(position), found.page);
11293
+ connection.onDefinition(async (request) => {
11294
+ const view = await openViewAt(request);
11295
+ if (!view) return null;
11296
+ return definition(view.document, view.offset, view.page);
10874
11297
  });
10875
11298
  documents.listen(connection);
10876
11299
  connection.listen();
10877
11300
  return connection;
10878
11301
  }
10879
- /** Serves an editor that started `dotctl lsp`, over stdin and stdout. */
10880
11302
  function serveStdio() {
10881
11303
  return startLanguageServer((0, import_node.createConnection)(import_node.ProposedFeatures.all, process.stdin, process.stdout));
10882
11304
  }
10883
11305
  //#endregion
10884
11306
  export { serveStdio };
10885
11307
 
10886
- //# sourceMappingURL=server-z7M4FLjf.js.map
11308
+ //# sourceMappingURL=server-DMnWDdJ6.js.map