tutuca 0.9.48 → 0.9.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,11 +73,9 @@ in the storybook shape `{ title, description?, items: [{ title, description?, va
73
73
 
74
74
  ```sh
75
75
  npm install --save-dev tutuca
76
- # prettier is optional, only needed for --pretty
77
- npm install --save-dev prettier
78
76
  ```
79
77
 
80
- The package exposes `tutuca` via `bin`, so `npx tutuca` (or a global `npm i -g tutuca`) just works. `jsdom` ships as a regular dependency (it's needed by `render` and `lint`) and is installed automatically.
78
+ The package exposes `tutuca` via `bin`, so `npx tutuca` (or a global `npm i -g tutuca`) just works. `jsdom` (needed by `render` and `lint`) and `prettier` (used by `--pretty`) ship as regular dependencies and are installed automatically.
81
79
 
82
80
  ### Commands
83
81
 
@@ -3344,11 +3344,12 @@ class ComponentDocs {
3344
3344
  }
3345
3345
 
3346
3346
  class LintFinding {
3347
- constructor({ id, level, info, context = {} }) {
3347
+ constructor({ id, level, info, context = {}, suggestion = null }) {
3348
3348
  this.id = id;
3349
3349
  this.level = level;
3350
3350
  this.info = info;
3351
3351
  this.context = context;
3352
+ this.suggestion = suggestion;
3352
3353
  }
3353
3354
  }
3354
3355
 
@@ -6633,10 +6634,7 @@ class LinterCtx {
6633
6634
  a.quote = quote;
6634
6635
  const dup = this.currentAttrs.find((x) => x.name === a.name);
6635
6636
  if (dup) {
6636
- this.report(HTML_DUPLICATE_ATTRIBUTE, LEVEL_WARN, a.nameStart, {
6637
- name: a.name,
6638
- firstAt: dup.nameStart
6639
- });
6637
+ this.report(HTML_DUPLICATE_ATTRIBUTE, LEVEL_WARN, a.nameStart, { name: a.name, firstAt: dup.nameStart }, { kind: "remove", what: `the duplicate '${a.name}' attribute` });
6640
6638
  } else {
6641
6639
  this.currentAttrs.push(a);
6642
6640
  }
@@ -6686,12 +6684,12 @@ class LinterCtx {
6686
6684
  }
6687
6685
  }
6688
6686
  if (firstNonWs < 0) {
6689
- this.report(HTML_SELF_CLOSING_END_TAG, LEVEL_WARN, start, { tag: name });
6687
+ this.report(HTML_SELF_CLOSING_END_TAG, LEVEL_WARN, start, { tag: name }, { kind: "rewrite", from: `</${name}/>`, to: `</${name}>` });
6690
6688
  } else {
6691
- this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name });
6689
+ this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name }, { kind: "remove", what: `attributes from </${name}>` });
6692
6690
  }
6693
6691
  } else {
6694
- this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name });
6692
+ this.report(HTML_ATTRIBUTES_ON_END_TAG, LEVEL_WARN, start, { tag: name }, { kind: "remove", what: `attributes from </${name}>` });
6695
6693
  }
6696
6694
  }
6697
6695
  this.handleEndTag(name, start);
@@ -6724,14 +6722,15 @@ class LinterCtx {
6724
6722
  return this.currentNamespace() !== NS.html;
6725
6723
  }
6726
6724
  onend() {}
6727
- report(id, level, offset, info) {
6725
+ report(id, level, offset, info, suggestion = null) {
6728
6726
  this.findingCount++;
6729
6727
  const { line, column } = offsetToLineCol(this.lineStarts, offset);
6730
6728
  this.onFinding({
6731
6729
  id,
6732
6730
  level,
6733
6731
  info,
6734
- location: { start: offset, end: offset + (info.tag?.length ?? 0), line, column }
6732
+ location: { start: offset, end: offset + (info.tag?.length ?? 0), line, column },
6733
+ suggestion
6735
6734
  });
6736
6735
  }
6737
6736
  currentNode() {
@@ -6840,17 +6839,11 @@ class LinterCtx {
6840
6839
  const ns = this.currentNamespace();
6841
6840
  if (ns === NS.html) {
6842
6841
  if (raw !== name) {
6843
- this.report(HTML_TAG_NAME_HAS_UPPERCASE, LEVEL_ERROR, start, {
6844
- raw,
6845
- lowercased: name
6846
- });
6842
+ this.report(HTML_TAG_NAME_HAS_UPPERCASE, LEVEL_ERROR, start, { raw, lowercased: name }, { kind: "rewrite", from: `<${raw}>`, to: `<${name}>` });
6847
6843
  }
6848
6844
  } else if (ns === NS.svg) {
6849
6845
  if (raw !== raw.toLowerCase() && !this.svgCamelElements.has(raw)) {
6850
- this.report(HTML_SVG_TAG_WILL_LOWERCASE, LEVEL_ERROR, start, {
6851
- raw,
6852
- lowercased: name
6853
- });
6846
+ this.report(HTML_SVG_TAG_WILL_LOWERCASE, LEVEL_ERROR, start, { raw, lowercased: name }, { kind: "rewrite", from: `<${raw}>`, to: `<${name}>` });
6854
6847
  }
6855
6848
  }
6856
6849
  const targetNs = ns !== NS.html ? ns : name === "svg" ? NS.svg : name === "math" ? NS.math : NS.html;
@@ -6858,20 +6851,14 @@ class LinterCtx {
6858
6851
  for (const a of this.currentAttrs) {
6859
6852
  const canonical = SVG_ATTR_LOWERCASE_TO_CAMEL.get(a.name);
6860
6853
  if (canonical && a.rawName !== canonical) {
6861
- this.report(HTML_SVG_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, {
6862
- raw: a.rawName,
6863
- canonical
6864
- });
6854
+ this.report(HTML_SVG_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, { raw: a.rawName, canonical }, { kind: "rewrite", from: a.rawName, to: canonical });
6865
6855
  }
6866
6856
  }
6867
6857
  } else if (targetNs === NS.math) {
6868
6858
  for (const a of this.currentAttrs) {
6869
6859
  const canonical = MATHML_ATTR_LOWERCASE_TO_CAMEL.get(a.name);
6870
6860
  if (canonical && a.rawName !== canonical) {
6871
- this.report(HTML_MATHML_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, {
6872
- raw: a.rawName,
6873
- canonical
6874
- });
6861
+ this.report(HTML_MATHML_ATTR_WILL_LOWERCASE, LEVEL_ERROR, a.nameStart, { raw: a.rawName, canonical }, { kind: "rewrite", from: a.rawName, to: canonical });
6875
6862
  }
6876
6863
  }
6877
6864
  }
@@ -6986,10 +6973,7 @@ class LinterCtx {
6986
6973
  }
6987
6974
  if (name === "form") {
6988
6975
  if (this.openElementsHas("form") && !this.openElementsHas("template")) {
6989
- this.report(HTML_DUPLICATE_FORM, LEVEL_ERROR, start, {
6990
- tag: raw,
6991
- mode: this.insertionMode
6992
- });
6976
+ this.report(HTML_DUPLICATE_FORM, LEVEL_ERROR, start, { tag: raw, mode: this.insertionMode }, { kind: "remove", what: "the inner <form>" });
6993
6977
  return;
6994
6978
  }
6995
6979
  if (this.hasInButtonScope("p"))
@@ -7551,10 +7535,7 @@ class LinterCtx {
7551
7535
  }
7552
7536
  }
7553
7537
  if (VOID_ELEMENTS.has(name)) {
7554
- this.report(HTML_VOID_ELEMENT_HAS_CLOSE_TAG, LEVEL_WARN, start, {
7555
- tag: name,
7556
- mode: this.insertionMode
7557
- });
7538
+ this.report(HTML_VOID_ELEMENT_HAS_CLOSE_TAG, LEVEL_WARN, start, { tag: name, mode: this.insertionMode }, { kind: "remove", what: `the </${name}>` });
7558
7539
  return;
7559
7540
  }
7560
7541
  if (FORMATTING_ELEMENTS.has(name)) {
@@ -7691,7 +7672,89 @@ var init_htmllinter = __esm(() => {
7691
7672
  ]);
7692
7673
  });
7693
7674
 
7675
+ // tools/core/util/closest-name.js
7676
+ function editDistance(a, b) {
7677
+ if (a === b)
7678
+ return 0;
7679
+ const la = a.length;
7680
+ const lb = b.length;
7681
+ if (la === 0)
7682
+ return lb;
7683
+ if (lb === 0)
7684
+ return la;
7685
+ const prev2 = new Array(lb + 1);
7686
+ const prev1 = new Array(lb + 1);
7687
+ const curr = new Array(lb + 1);
7688
+ for (let j = 0;j <= lb; j++)
7689
+ prev1[j] = j;
7690
+ for (let i = 1;i <= la; i++) {
7691
+ curr[0] = i;
7692
+ const ca = a.charCodeAt(i - 1);
7693
+ for (let j = 1;j <= lb; j++) {
7694
+ const cb = b.charCodeAt(j - 1);
7695
+ const cost = ca === cb ? 0 : 1;
7696
+ let v = Math.min(curr[j - 1] + 1, prev1[j] + 1, prev1[j - 1] + cost);
7697
+ if (i > 1 && j > 1 && ca === b.charCodeAt(j - 2) && a.charCodeAt(i - 2) === cb) {
7698
+ v = Math.min(v, prev2[j - 2] + 1);
7699
+ }
7700
+ curr[j] = v;
7701
+ }
7702
+ for (let j = 0;j <= lb; j++) {
7703
+ prev2[j] = prev1[j];
7704
+ prev1[j] = curr[j];
7705
+ }
7706
+ }
7707
+ return prev1[lb];
7708
+ }
7709
+ function closestName(name, candidates, maxDistance = 2) {
7710
+ if (!name)
7711
+ return null;
7712
+ const lower = name.toLowerCase();
7713
+ let best = null;
7714
+ let bestDist = maxDistance + 1;
7715
+ for (const cand of candidates) {
7716
+ if (cand === name)
7717
+ return null;
7718
+ if (cand.toLowerCase() === lower)
7719
+ return cand;
7720
+ const d = editDistance(name, cand);
7721
+ if (d < bestDist) {
7722
+ best = cand;
7723
+ bestDist = d;
7724
+ }
7725
+ }
7726
+ return bestDist <= maxDistance ? best : null;
7727
+ }
7728
+
7694
7729
  // tools/core/lint-check.js
7730
+ function collectProtoMethodNames(proto) {
7731
+ const out = [];
7732
+ let cursor = proto;
7733
+ while (cursor && cursor !== Object.prototype) {
7734
+ for (const key of Object.getOwnPropertyNames(cursor)) {
7735
+ if (key === "constructor")
7736
+ continue;
7737
+ out.push(key);
7738
+ }
7739
+ cursor = Object.getPrototypeOf(cursor);
7740
+ }
7741
+ return out;
7742
+ }
7743
+ function scopeKeysAlong(scope, mapKey) {
7744
+ const out = [];
7745
+ for (let cursor = scope;cursor; cursor = cursor.parent) {
7746
+ const map = cursor[mapKey];
7747
+ if (!map)
7748
+ continue;
7749
+ for (const key of Object.keys(map))
7750
+ out.push(key);
7751
+ }
7752
+ return out;
7753
+ }
7754
+ function replaceNameSuggestion(name, candidates) {
7755
+ const close = closestName(name, candidates);
7756
+ return close ? { kind: "replace-name", from: name, to: close } : null;
7757
+ }
7695
7758
  function checkComponent(Comp, lx = new LintContext) {
7696
7759
  return lx.push({ componentName: Comp.name }, () => {
7697
7760
  const referencedAlters = new Set;
@@ -7717,7 +7780,7 @@ function checkView(lx, view, Comp, referencedAlters) {
7717
7780
  function checkHtmlStructure(lx, view) {
7718
7781
  if (typeof view.rawView !== "string" || !view.rawView)
7719
7782
  return;
7720
- lintHtml(view.rawView, (f) => lx.report(f.id, { ...f.info, location: f.location }, f.level), HTML_LINT_OPTS);
7783
+ lintHtml(view.rawView, (f) => lx.report(f.id, { ...f.info, location: f.location }, f.level, f.suggestion ?? null), HTML_LINT_OPTS);
7721
7784
  }
7722
7785
  function checkParseIssues(lx, view) {
7723
7786
  const issues = view.ctx.parseIssues;
@@ -7727,12 +7790,19 @@ function checkParseIssues(lx, view) {
7727
7790
  const id = PARSE_ISSUE_KIND_TO_LINT_ID[kind];
7728
7791
  if (!id)
7729
7792
  continue;
7730
- lx.error(id, info);
7731
- const known = AT_PREFIX_HINT_KNOWN_BY_KIND[kind];
7732
- if (known && info.name?.startsWith("@")) {
7733
- const suggestion = info.name.slice(1);
7734
- if (known.has(suggestion))
7735
- lx.hint(MAYBE_DROP_AT_PREFIX, { ...info, suggestion });
7793
+ const atPrefixKnown = AT_PREFIX_HINT_KNOWN_BY_KIND[kind];
7794
+ const isAtPrefixedTypo = atPrefixKnown && info.name?.startsWith("@") && atPrefixKnown.has(info.name.slice(1));
7795
+ let suggestion = null;
7796
+ if (isAtPrefixedTypo) {
7797
+ suggestion = { kind: "drop-prefix", from: info.name, to: info.name.slice(1) };
7798
+ } else {
7799
+ const candidates = PARSE_ISSUE_KIND_TO_KNOWN_NAMES[kind];
7800
+ if (candidates)
7801
+ suggestion = replaceNameSuggestion(info.name, candidates);
7802
+ }
7803
+ lx.error(id, info, suggestion);
7804
+ if (isAtPrefixedTypo) {
7805
+ lx.hint(MAYBE_DROP_AT_PREFIX, { ...info, suggestion: info.name.slice(1) }, { kind: "drop-prefix", from: info.name, to: info.name.slice(1) });
7736
7806
  }
7737
7807
  }
7738
7808
  }
@@ -7745,11 +7815,7 @@ function checkMacroCallArgs(lx, view, Comp) {
7745
7815
  const { defaults } = macro;
7746
7816
  for (const argName in macroNode.attrs) {
7747
7817
  if (!(argName in defaults)) {
7748
- lx.error(UNKNOWN_MACRO_ARG, {
7749
- name: argName,
7750
- macroName: macroNode.name,
7751
- tag: `x:${macroNode.name}`
7752
- });
7818
+ lx.error(UNKNOWN_MACRO_ARG, { name: argName, macroName: macroNode.name, tag: `x:${macroNode.name}` }, replaceNameSuggestion(argName, Object.keys(defaults)));
7753
7819
  }
7754
7820
  }
7755
7821
  }
@@ -7771,8 +7837,9 @@ function walkForRenderIt(lx, node, loopDepth) {
7771
7837
  return;
7772
7838
  switch (node.constructor.name) {
7773
7839
  case "RenderItNode":
7774
- if (loopDepth === 0)
7775
- lx.error(RENDER_IT_OUTSIDE_OF_LOOP, { node });
7840
+ if (loopDepth === 0) {
7841
+ lx.error(RENDER_IT_OUTSIDE_OF_LOOP, { node }, { kind: "wrap", from: "<x render-it>", to: "<x render-each>" });
7842
+ }
7776
7843
  return;
7777
7844
  case "EachNode":
7778
7845
  walkForRenderIt(lx, node.node, loopDepth + 1);
@@ -7802,13 +7869,14 @@ function checkEventModifiers(lx, view) {
7802
7869
  const modWrappers = MOD_WRAPPERS_BY_EVENT[name] ?? NO_WRAPPERS;
7803
7870
  for (const modifier of modifiers) {
7804
7871
  if (modWrappers[modifier] === undefined) {
7872
+ const close = closestName(modifier, Object.keys(modWrappers));
7805
7873
  lx.error(UNKNOWN_EVENT_MODIFIER, {
7806
7874
  name,
7807
7875
  modifier,
7808
7876
  handler,
7809
7877
  event,
7810
7878
  originAttr: `@on.${name}+${modifiers.join("+")}`
7811
- });
7879
+ }, close ? { kind: "replace-name", from: `+${modifier}`, to: `+${close}` } : null);
7812
7880
  }
7813
7881
  }
7814
7882
  }
@@ -7854,42 +7922,22 @@ function checkEventHandlersHaveImpls(lx, Comp, referencedInputs) {
7854
7922
  const originAttr = `@on.${eventName}`;
7855
7923
  if (hvName === "InputHandlerNameVal") {
7856
7924
  referencedInputs?.add(handlerVal.name);
7857
- if (input[handlerVal.name] === undefined) {
7858
- lx.error(INPUT_HANDLER_NOT_IMPLEMENTED, {
7859
- name: handlerVal.name,
7860
- handler,
7861
- event,
7862
- eventName,
7863
- originAttr
7864
- });
7865
- if (proto[handlerVal.name] !== undefined) {
7866
- lx.hint(INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER, {
7867
- name: handlerVal.name,
7868
- handler,
7869
- event,
7870
- eventName,
7871
- originAttr
7872
- });
7925
+ const { name } = handlerVal;
7926
+ if (input[name] === undefined) {
7927
+ const isMethodFix = proto[name] !== undefined;
7928
+ lx.error(INPUT_HANDLER_NOT_IMPLEMENTED, { name, handler, event, eventName, originAttr }, isMethodFix ? { kind: "add-prefix", from: name, to: `.${name}` } : replaceNameSuggestion(name, Object.keys(input)));
7929
+ if (isMethodFix) {
7930
+ lx.hint(INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER, { name, handler, event, eventName, originAttr }, { kind: "add-prefix", from: name, to: `.${name}` });
7873
7931
  }
7874
7932
  }
7875
7933
  } else if (hvName === "RawFieldVal") {
7876
7934
  referencedInputs?.add(handlerVal.name);
7877
- if (proto[handlerVal.name] === undefined) {
7878
- lx.error(INPUT_HANDLER_METHOD_NOT_IMPLEMENTED, {
7879
- name: handlerVal.name,
7880
- handler,
7881
- event,
7882
- eventName,
7883
- originAttr
7884
- });
7885
- if (input[handlerVal.name] !== undefined) {
7886
- lx.hint(INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD, {
7887
- name: handlerVal.name,
7888
- handler,
7889
- event,
7890
- eventName,
7891
- originAttr
7892
- });
7935
+ const { name } = handlerVal;
7936
+ if (proto[name] === undefined) {
7937
+ const isInputFix = input[name] !== undefined;
7938
+ lx.error(INPUT_HANDLER_METHOD_NOT_IMPLEMENTED, { name, handler, event, eventName, originAttr }, isInputFix ? { kind: "drop-prefix", from: `.${name}`, to: name } : replaceNameSuggestion(name, collectProtoMethodNames(proto)));
7939
+ if (isInputFix) {
7940
+ lx.hint(INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD, { name, handler, event, eventName, originAttr }, { kind: "drop-prefix", from: `.${name}`, to: name });
7893
7941
  }
7894
7942
  }
7895
7943
  }
@@ -7903,22 +7951,23 @@ function checkConsistentAttrVal(lx, val, fields, proto, scope, alter, referenced
7903
7951
  if (valName === "FieldVal" || valName === "RawFieldVal") {
7904
7952
  const { name } = val;
7905
7953
  if (fields[name] === undefined && proto[name] === undefined) {
7906
- lx.error(FIELD_VAL_NOT_DEFINED, { ...errCtx, val, name });
7954
+ const candidates = [...Object.keys(fields), ...collectProtoMethodNames(proto)];
7955
+ lx.error(FIELD_VAL_NOT_DEFINED, { ...errCtx, val, name }, replaceNameSuggestion(name, candidates));
7907
7956
  }
7908
7957
  } else if (valName === "SeqAccessVal") {
7909
7958
  checkConsistentAttrVal(lx, val.seqVal, fields, proto, scope, alter, referencedAlters, skipNameVal, errCtx);
7910
7959
  checkConsistentAttrVal(lx, val.keyVal, fields, proto, scope, alter, referencedAlters, skipNameVal, errCtx);
7911
7960
  } else if (valName === "RequestVal") {
7912
7961
  if (scope.lookupRequest(val.name) === null) {
7913
- lx.error(UNKNOWN_REQUEST_NAME, { ...errCtx, name: val.name });
7962
+ lx.error(UNKNOWN_REQUEST_NAME, { ...errCtx, name: val.name }, replaceNameSuggestion(val.name, scopeKeysAlong(scope, "reqsByName")));
7914
7963
  }
7915
7964
  } else if (valName === "TypeVal") {
7916
7965
  if (scope.lookupComponent(val.name) === null) {
7917
- lx.error(UNKNOWN_COMPONENT_NAME, { ...errCtx, name: val.name });
7966
+ lx.error(UNKNOWN_COMPONENT_NAME, { ...errCtx, name: val.name }, replaceNameSuggestion(val.name, scopeKeysAlong(scope, "byName")));
7918
7967
  }
7919
7968
  } else if (valName === "NameVal") {
7920
7969
  if (!skipNameVal && !isKnownHandlerName(val.name)) {
7921
- lx.error(UNKNOWN_HANDLER_ARG_NAME, { ...errCtx, name: val.name });
7970
+ lx.error(UNKNOWN_HANDLER_ARG_NAME, { ...errCtx, name: val.name }, replaceNameSuggestion(val.name, KNOWN_HANDLER_NAMES));
7922
7971
  }
7923
7972
  } else if (valName === "StrTplVal") {
7924
7973
  for (const subVal of val.vals) {
@@ -7927,7 +7976,7 @@ function checkConsistentAttrVal(lx, val, fields, proto, scope, alter, referenced
7927
7976
  } else if (valName === "AlterHandlerNameVal") {
7928
7977
  referencedAlters?.add(val.name);
7929
7978
  if (alter[val.name] === undefined) {
7930
- lx.error(ALT_HANDLER_NOT_DEFINED, { ...errCtx, name: val.name });
7979
+ lx.error(ALT_HANDLER_NOT_DEFINED, { ...errCtx, name: val.name }, replaceNameSuggestion(val.name, Object.keys(alter)));
7931
7980
  }
7932
7981
  } else if (valName !== "ConstVal" && valName !== "BindVal" && valName !== "DynVal") {
7933
7982
  console.log(val);
@@ -8064,23 +8113,37 @@ class LintContext {
8064
8113
  this.frame = prev;
8065
8114
  }
8066
8115
  }
8067
- error(id, info) {
8068
- this.report(id, info, LEVEL_ERROR2);
8116
+ error(id, info, suggestion = null) {
8117
+ this.report(id, info, LEVEL_ERROR2, suggestion);
8069
8118
  }
8070
- warn(id, info) {
8071
- this.report(id, info, LEVEL_WARN2);
8119
+ warn(id, info, suggestion = null) {
8120
+ this.report(id, info, LEVEL_WARN2, suggestion);
8072
8121
  }
8073
- hint(id, info) {
8074
- this.report(id, info, LEVEL_HINT);
8122
+ hint(id, info, suggestion = null) {
8123
+ this.report(id, info, LEVEL_HINT, suggestion);
8075
8124
  }
8076
- report(id, info = {}, level = LEVEL_ERROR2) {
8077
- this.reports.push({ id, info, level, context: { ...this.frame } });
8125
+ report(id, info = {}, level = LEVEL_ERROR2, suggestion = null) {
8126
+ this.reports.push({ id, info, level, context: { ...this.frame }, suggestion });
8078
8127
  }
8079
8128
  }
8080
- var ALT_HANDLER_NOT_DEFINED = "ALT_HANDLER_NOT_DEFINED", ALT_HANDLER_NOT_REFERENCED = "ALT_HANDLER_NOT_REFERENCED", RENDER_IT_OUTSIDE_OF_LOOP = "RENDER_IT_OUTSIDE_OF_LOOP", UNKNOWN_EVENT_MODIFIER = "UNKNOWN_EVENT_MODIFIER", UNKNOWN_HANDLER_ARG_NAME = "UNKNOWN_HANDLER_ARG_NAME", INPUT_HANDLER_NOT_IMPLEMENTED = "INPUT_HANDLER_NOT_IMPLEMENTED", INPUT_HANDLER_NOT_REFERENCED = "INPUT_HANDLER_NOT_REFERENCED", INPUT_HANDLER_METHOD_NOT_IMPLEMENTED = "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED", INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD = "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD", INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER = "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER", FIELD_VAL_NOT_DEFINED = "FIELD_VAL_NOT_DEFINED", DUPLICATE_ATTR_DEFINITION = "DUPLICATE_ATTR_DEFINITION", IF_NO_BRANCH_SET = "IF_NO_BRANCH_SET", UNKNOWN_REQUEST_NAME = "UNKNOWN_REQUEST_NAME", UNKNOWN_COMPONENT_NAME = "UNKNOWN_COMPONENT_NAME", UNKNOWN_MACRO_ARG = "UNKNOWN_MACRO_ARG", UNKNOWN_DIRECTIVE = "UNKNOWN_DIRECTIVE", UNKNOWN_X_OP = "UNKNOWN_X_OP", UNKNOWN_X_ATTR = "UNKNOWN_X_ATTR", MAYBE_DROP_AT_PREFIX = "MAYBE_DROP_AT_PREFIX", BAD_VALUE = "BAD_VALUE", PARSE_ISSUE_KIND_TO_LINT_ID, X_KNOWN_OP_NAMES, X_KNOWN_ATTR_NAMES, AT_PREFIX_HINT_KNOWN_BY_KIND, LEVEL_WARN2 = "warn", LEVEL_ERROR2 = "error", LEVEL_HINT = "hint", HTML_LINT_OPTS, NO_WRAPPERS, KNOWN_HANDLER_NAMES, NODE_KIND_TO_CTX, LintParseContext;
8129
+ var KNOWN_DIRECTIVE_NAMES, ALT_HANDLER_NOT_DEFINED = "ALT_HANDLER_NOT_DEFINED", ALT_HANDLER_NOT_REFERENCED = "ALT_HANDLER_NOT_REFERENCED", RENDER_IT_OUTSIDE_OF_LOOP = "RENDER_IT_OUTSIDE_OF_LOOP", UNKNOWN_EVENT_MODIFIER = "UNKNOWN_EVENT_MODIFIER", UNKNOWN_HANDLER_ARG_NAME = "UNKNOWN_HANDLER_ARG_NAME", INPUT_HANDLER_NOT_IMPLEMENTED = "INPUT_HANDLER_NOT_IMPLEMENTED", INPUT_HANDLER_NOT_REFERENCED = "INPUT_HANDLER_NOT_REFERENCED", INPUT_HANDLER_METHOD_NOT_IMPLEMENTED = "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED", INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD = "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD", INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER = "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER", FIELD_VAL_NOT_DEFINED = "FIELD_VAL_NOT_DEFINED", DUPLICATE_ATTR_DEFINITION = "DUPLICATE_ATTR_DEFINITION", IF_NO_BRANCH_SET = "IF_NO_BRANCH_SET", UNKNOWN_REQUEST_NAME = "UNKNOWN_REQUEST_NAME", UNKNOWN_COMPONENT_NAME = "UNKNOWN_COMPONENT_NAME", UNKNOWN_MACRO_ARG = "UNKNOWN_MACRO_ARG", UNKNOWN_DIRECTIVE = "UNKNOWN_DIRECTIVE", UNKNOWN_X_OP = "UNKNOWN_X_OP", UNKNOWN_X_ATTR = "UNKNOWN_X_ATTR", MAYBE_DROP_AT_PREFIX = "MAYBE_DROP_AT_PREFIX", BAD_VALUE = "BAD_VALUE", PARSE_ISSUE_KIND_TO_LINT_ID, X_KNOWN_OP_NAMES, X_KNOWN_ATTR_NAMES, AT_PREFIX_HINT_KNOWN_BY_KIND, LEVEL_WARN2 = "warn", LEVEL_ERROR2 = "error", LEVEL_HINT = "hint", PARSE_ISSUE_KIND_TO_KNOWN_NAMES, HTML_LINT_OPTS, NO_WRAPPERS, KNOWN_HANDLER_NAMES, NODE_KIND_TO_CTX, LintParseContext;
8081
8130
  var init_lint_check = __esm(() => {
8082
8131
  init_anode();
8083
8132
  init_htmllinter();
8133
+ KNOWN_DIRECTIVE_NAMES = new Set([
8134
+ "dangerouslysetinnerhtml",
8135
+ "slot",
8136
+ "push-view",
8137
+ "text",
8138
+ "show",
8139
+ "hide",
8140
+ "each",
8141
+ "enrich-with",
8142
+ "when",
8143
+ "loop-with",
8144
+ "then",
8145
+ "else"
8146
+ ]);
8084
8147
  PARSE_ISSUE_KIND_TO_LINT_ID = {
8085
8148
  "unknown-directive": UNKNOWN_DIRECTIVE,
8086
8149
  "unknown-x-op": UNKNOWN_X_OP,
@@ -8101,6 +8164,11 @@ var init_lint_check = __esm(() => {
8101
8164
  "unknown-x-op": X_KNOWN_OP_NAMES,
8102
8165
  "unknown-x-attr": X_KNOWN_ATTR_NAMES
8103
8166
  };
8167
+ PARSE_ISSUE_KIND_TO_KNOWN_NAMES = {
8168
+ "unknown-directive": KNOWN_DIRECTIVE_NAMES,
8169
+ "unknown-x-op": X_KNOWN_OP_NAMES,
8170
+ "unknown-x-attr": X_KNOWN_ATTR_NAMES
8171
+ };
8104
8172
  HTML_LINT_OPTS = {
8105
8173
  fragmentContext: "template",
8106
8174
  transparentTagPrefixes: ["x"]
@@ -14268,7 +14336,6 @@ GLOBAL FLAGS
14268
14336
  -o, --output <file> Write to <file> instead of stdout.
14269
14337
  --pretty Pretty-print HTML (md/html formats) via
14270
14338
  prettier; JSON formatter uses indent 2.
14271
- Requires \`prettier\` to be installed.
14272
14339
  -h, --help Show this help.
14273
14340
  --module <path> Alternative to first-positional module path.
14274
14341
 
@@ -14279,9 +14346,6 @@ EXIT CODES
14279
14346
  3 render crash
14280
14347
  4 test failures
14281
14348
 
14282
- ENVIRONMENT
14283
- \`prettier\` is an optional peer dep, only used by --pretty.
14284
-
14285
14349
  EXAMPLES
14286
14350
  # Inspect a module
14287
14351
  tutuca ./src/components.js info
@@ -14456,7 +14520,7 @@ function fmtEventSuffix(info) {
14456
14520
  function lintIdToMessage(id, info) {
14457
14521
  switch (id) {
14458
14522
  case "RENDER_IT_OUTSIDE_OF_LOOP":
14459
- return "render-it used outside of a loop";
14523
+ return "<x render-it> used outside of a loop";
14460
14524
  case "UNKNOWN_EVENT_MODIFIER": {
14461
14525
  const mods = info.handler?.modifiers ?? [info.modifier];
14462
14526
  const written = `@on.${info.name}+${mods.join("+")}`;
@@ -14467,13 +14531,13 @@ function lintIdToMessage(id, info) {
14467
14531
  case "INPUT_HANDLER_NOT_IMPLEMENTED":
14468
14532
  return `Input handler '${info.name}' is not implemented${fmtEventSuffix(info)}`;
14469
14533
  case "INPUT_HANDLER_NOT_REFERENCED":
14470
- return `Input handler '${info.name}' is defined but not referenced`;
14534
+ return `Input handler '${info.name}' is defined but never used — remove it or wire it to an @on.* event`;
14471
14535
  case "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED":
14472
14536
  return `Method '.${info.name}' is not implemented${fmtEventSuffix(info)}`;
14473
14537
  case "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD":
14474
- return `'${info.name}' exists as input handler use without '.' prefix${fmtEventSuffix(info)}`;
14538
+ return `'.${info.name}' is a method reference, but '${info.name}' is defined as an input handler${fmtEventSuffix(info)}`;
14475
14539
  case "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER":
14476
- return `'${info.name}' exists as method use with '.' prefix${fmtEventSuffix(info)}`;
14540
+ return `'${info.name}' is an input handler reference, but '${info.name}' is defined as a method${fmtEventSuffix(info)}`;
14477
14541
  case "FIELD_VAL_NOT_DEFINED":
14478
14542
  return `Field '.${info.name}' is not defined${fmtOriginSuffix(info)}`;
14479
14543
  case "DUPLICATE_ATTR_DEFINITION": {
@@ -14482,7 +14546,7 @@ function lintIdToMessage(id, info) {
14482
14546
  return `Attribute '${info.name}' is set ${info.sources?.length ?? "multiple"} times${sources}${tag}`;
14483
14547
  }
14484
14548
  case "IF_NO_BRANCH_SET":
14485
- return `'@if.${info.attr}' has no '@then' or '@else' branch — at least one must be set${fmtTagSuffix(info)}`;
14549
+ return `'@if.${info.attr}' has no '@then' or '@else' branch — add '@then="…"' or '@else="…"' (or both)${fmtTagSuffix(info)}`;
14486
14550
  case "UNKNOWN_REQUEST_NAME":
14487
14551
  return `Unknown request '!${info.name}'${fmtOriginSuffix(info)}`;
14488
14552
  case "UNKNOWN_COMPONENT_NAME":
@@ -14490,7 +14554,7 @@ function lintIdToMessage(id, info) {
14490
14554
  case "ALT_HANDLER_NOT_DEFINED":
14491
14555
  return `Alter handler '${info.name}' is not defined${fmtOriginSuffix(info)}`;
14492
14556
  case "ALT_HANDLER_NOT_REFERENCED":
14493
- return `Alter handler '${info.name}' is defined but not referenced`;
14557
+ return `Alter handler '${info.name}' is defined but never used — remove it or reference it from @when, @enrich-with, or @loop-with`;
14494
14558
  case "UNKNOWN_MACRO_ARG":
14495
14559
  return `Argument '${info.name}' is not declared in macro '${info.macroName}'`;
14496
14560
  case "UNKNOWN_DIRECTIVE":
@@ -14501,7 +14565,7 @@ function lintIdToMessage(id, info) {
14501
14565
  return `Unknown attribute '${info.name}=${JSON.stringify(info.value)}' on <x ${info.op}>${fmtTagSuffix(info)}`;
14502
14566
  case "MAYBE_DROP_AT_PREFIX": {
14503
14567
  const written = info.value !== undefined ? `${info.name}=${JSON.stringify(info.value)}` : info.name;
14504
- return `Did you mean '${info.suggestion}'? Drop the '@' from '${written}' on <x>.`;
14568
+ return `'${written}' on <x> looks like a directive but is actually an x op/attr written with a leading '@'`;
14505
14569
  }
14506
14570
  case "BAD_VALUE":
14507
14571
  return `${badValueMessage(info)}${fmtTagSuffix(info)}`;
@@ -14510,7 +14574,7 @@ function lintIdToMessage(id, info) {
14510
14574
  case "HTML_SVG_TAG_WILL_LOWERCASE":
14511
14575
  return `SVG tag <${info.raw}> is not in the WHATWG case-correction list — will be lowercased to <${info.lowercased}>${fmtLocationSuffix(info)}`;
14512
14576
  case "HTML_TAG_NOT_ALLOWED_IN_PARENT":
14513
- return `<${info.tag}> not allowed under <${info.parent ?? "?"}> in ${info.mode} (action: ${info.action})${fmtLocationSuffix(info)}`;
14577
+ return `<${info.tag}> not allowed under <${info.parent ?? "?"}> in ${info.mode} ${htmlActionPhrase(info.action, info.tag, info.parent)}${fmtLocationSuffix(info)}`;
14514
14578
  case "HTML_TEXT_NOT_ALLOWED_IN_PARENT":
14515
14579
  return `Non-whitespace text not allowed in ${info.mode}: ${JSON.stringify(info.snippet)}${fmtLocationSuffix(info)}`;
14516
14580
  case "HTML_VOID_ELEMENT_HAS_CLOSE_TAG":
@@ -14547,11 +14611,47 @@ function lintIdToMessage(id, info) {
14547
14611
  return id;
14548
14612
  }
14549
14613
  }
14614
+ function suggestionToMessage(suggestion) {
14615
+ if (!suggestion)
14616
+ return null;
14617
+ switch (suggestion.kind) {
14618
+ case "replace-name":
14619
+ return `did you mean '${suggestion.to}'?`;
14620
+ case "drop-prefix":
14621
+ return `did you mean '${suggestion.to}'? (drop the leading '${suggestion.from.slice(0, suggestion.from.length - suggestion.to.length)}')`;
14622
+ case "add-prefix":
14623
+ return `did you mean '${suggestion.to}'? (add the leading '${suggestion.to.slice(0, suggestion.to.length - suggestion.from.length)}')`;
14624
+ case "remove":
14625
+ return `remove ${suggestion.what}`;
14626
+ case "rewrite":
14627
+ return `use '${suggestion.to}' instead of '${suggestion.from}'`;
14628
+ case "wrap":
14629
+ return `wrap it in ${suggestion.to}`;
14630
+ default:
14631
+ return null;
14632
+ }
14633
+ }
14550
14634
  function fmtLocationSuffix(info) {
14551
14635
  const loc = info?.location;
14552
14636
  if (!loc)
14553
14637
  return "";
14554
- return ` at L${loc.line}:C${loc.column}`;
14638
+ return ` at line ${loc.line}, col ${loc.column}`;
14639
+ }
14640
+ function htmlActionPhrase(action, tag, parent) {
14641
+ switch (action) {
14642
+ case "ignored":
14643
+ return `the parser will drop this <${tag}>`;
14644
+ case "drop":
14645
+ return `the parser will drop this <${tag}>`;
14646
+ case "auto-close-implicit":
14647
+ return `the parser will close <${parent ?? "?"}> first, then place <${tag}> as a sibling`;
14648
+ case "foster-parent":
14649
+ return `the parser will move <${tag}> outside <${parent ?? "?"}> (foster-parenting)`;
14650
+ case "foreign-breakout":
14651
+ return `the parser will exit foreign content and re-process <${tag}> in HTML mode`;
14652
+ default:
14653
+ return `parser action: ${action}`;
14654
+ }
14555
14655
  }
14556
14656
 
14557
14657
  // tools/format/cli.js
@@ -14636,7 +14736,9 @@ function fmtLintReport(rep) {
14636
14736
  for (const f of c.findings) {
14637
14737
  const tag = f.level.toUpperCase();
14638
14738
  const view = f.context?.viewName ? ` [view=${f.context.viewName}]` : "";
14639
- lines.push(` [${tag}]${view} ${lintIdToMessage(f.id, f.info)}`);
14739
+ const tail = suggestionToMessage(f.suggestion);
14740
+ const suffix = tail ? ` — ${tail}` : "";
14741
+ lines.push(` [${tag}]${view} ${lintIdToMessage(f.id, f.info)}${suffix}`);
14640
14742
  }
14641
14743
  }
14642
14744
  lines.push("");
@@ -14817,7 +14919,9 @@ function fmtLintReport2(rep) {
14817
14919
  }
14818
14920
  for (const f of c.findings) {
14819
14921
  const view = f.context?.viewName ? ` _(view: \`${f.context.viewName}\`)_` : "";
14820
- lines.push(`- **${f.level.toUpperCase()}** ${lintIdToMessage(f.id, f.info)}${view}`);
14922
+ const tail = suggestionToMessage(f.suggestion);
14923
+ const suffix = tail ? ` — ${tail}` : "";
14924
+ lines.push(`- **${f.level.toUpperCase()}** ${lintIdToMessage(f.id, f.info)}${suffix}${view}`);
14821
14925
  }
14822
14926
  lines.push("");
14823
14927
  }