@wcstack/lint 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.cjs +169 -42
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -110,6 +110,9 @@ var WcsDiagnosticCode = {
110
110
  // --- built-in wcs-* tag contract (generated/builtinTags.generated.ts が正本) ---
111
111
  // 未知メンバーへのバインド(プロパティ / command. / eventToken. キー)。黙って無視される。
112
112
  TagMemberUnknown: "wcs/tag-member-unknown",
113
+ // wcBindable 無宣言タグ(wcs-fetch-header 等のヘルパー)への spread。
114
+ // ランタイム(expandSpread)は raiseError で落とす。
115
+ SpreadNoBindable: "wcs/spread-no-bindable",
113
116
  // trigger バインド先スロットの true シード(エッジ検出なし・manual バイパスで即発火)。
114
117
  TriggerSeededTruthy: "wcs/trigger-seeded-truthy",
115
118
  // 非 manual <wcs-storage> value バインド先の空値シード(初期書き戻しが保存値を上書き)。
@@ -310,6 +313,8 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
310
313
  const jsonAttr = extractAttribute(wcsMatch.tagContent, "json") ?? void 0;
311
314
  const stateAttr = extractAttribute(wcsMatch.tagContent, "state") ?? void 0;
312
315
  const srcAttr = extractAttribute(wcsMatch.tagContent, "src") ?? void 0;
316
+ const tagStart = pos;
317
+ const tagEnd = wcsMatch.end;
313
318
  pos = wcsMatch.end;
314
319
  const scriptBlocks = [];
315
320
  const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
@@ -346,7 +351,7 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
346
351
  pos = html.indexOf(">", scriptCloseIdx) + 1;
347
352
  if (pos === 0) break;
348
353
  }
349
- elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks });
354
+ elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks, tagStart, tagEnd });
350
355
  pos = wcsEnd;
351
356
  if (wcsCloseIdx !== -1) {
352
357
  const closeEnd = html.indexOf(">", wcsCloseIdx);
@@ -515,6 +520,26 @@ function isNonFunctionLiteral(value) {
515
520
  if (/^(?:async\s+)?function\b/.test(trimmed) || scan.includes("=>")) return false;
516
521
  return /^["'`]/.test(trimmed) || /^-?\d/.test(trimmed) || /^(?:true|false|null|undefined)\b/.test(trimmed) || trimmed.startsWith("[") || trimmed.startsWith("{");
517
522
  }
523
+ function findNonObjectWatch(scriptContent) {
524
+ const root = locateDefaultExportObject(scriptContent);
525
+ if (!root) return null;
526
+ const watchProp = parseTopLevelProperties(root.content).find((p) => p.name === RESERVED_WATCH_KEY);
527
+ if (!watchProp || watchProp.nameStart === void 0 || watchProp.nameEnd === void 0) {
528
+ return null;
529
+ }
530
+ const span = { start: root.start + watchProp.nameStart, end: root.start + watchProp.nameEnd };
531
+ if (watchProp.kind === "method") {
532
+ return span;
533
+ }
534
+ if (watchProp.kind !== "data" || !watchProp.value) return null;
535
+ const trimmed = watchProp.value.trim();
536
+ if (trimmed.startsWith("{")) return null;
537
+ const scan = maskCommentsAndStrings(trimmed).trim();
538
+ const isArrowFunction = /^(?:async\s+)?\([^()]*\)\s*=>/.test(scan) || /^(?:async\s+)?[$\w]+\s*=>/.test(scan);
539
+ const isWholeLiteral = /^(["'`])[^"'`]*\1$/.test(scan) || /^-?\d[\w.]*$/.test(scan) || /^(?:true|false|null)$/.test(scan) || /^(?:async\s+)?function\b[\s\S]*\}$/.test(scan);
540
+ if (!isArrowFunction && !isWholeLiteral) return null;
541
+ return span;
542
+ }
518
543
  function collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys, stateName) {
519
544
  if (prop.name === RESERVED_STREAMS_KEY && prop.kind === "data" && prop.value && isObjectLiteral(prop.value)) {
520
545
  const entries = parseTopLevelProperties(extractObjectContent(prop.value));
@@ -598,14 +623,9 @@ function pushDataPropertyPathsAt(path, prop, paths, stateName, depth) {
598
623
  if (prop.value && isArrayLiteral(prop.value)) {
599
624
  paths.push({ path: `${path}.*`, kind: "list", stateName });
600
625
  paths.push({ path: `${path}.length`, kind: "data", typeHint: "number", stateName });
601
- const elementProps = extractArrayElementProperties(prop.value);
602
- for (const childProp of elementProps) {
603
- paths.push({
604
- path: `${path}.*.${childProp.name}`,
605
- kind: "data",
606
- typeHint: childProp.typeHint,
607
- stateName
608
- });
626
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
627
+ for (const childProp of extractArrayElementDataProperties(prop.value)) {
628
+ pushDataPropertyPathsAt(`${path}.*.${childProp.name}`, childProp, paths, stateName, depth + 1);
609
629
  }
610
630
  return;
611
631
  }
@@ -631,25 +651,27 @@ function analyzeJsonPaths(jsonString, stateName = "default") {
631
651
  return paths;
632
652
  }
633
653
  function collectJsonPaths(obj, prefix, paths, stateName, depth) {
634
- if (depth > 5) return;
654
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
635
655
  for (const [key, value] of Object.entries(obj)) {
636
656
  if (prefix === "" && key.startsWith("$")) continue;
637
657
  const path = prefix ? `${prefix}.${key}` : key;
638
- const typeHint = inferJsonTypeHint(value);
639
- paths.push({ path, kind: "data", typeHint, stateName });
640
- if (Array.isArray(value)) {
641
- paths.push({ path: `${path}.*`, kind: "list", stateName });
642
- paths.push({ path: `${path}.length`, kind: "data", typeHint: "number", stateName });
643
- if (value.length > 0 && typeof value[0] === "object" && value[0] !== null && !Array.isArray(value[0])) {
644
- const firstElement = value[0];
645
- for (const [childKey, childValue] of Object.entries(firstElement)) {
646
- const childPath = `${path}.*.${childKey}`;
647
- paths.push({ path: childPath, kind: "data", typeHint: inferJsonTypeHint(childValue), stateName });
648
- }
658
+ pushJsonValuePaths(path, value, paths, stateName, depth);
659
+ }
660
+ }
661
+ function pushJsonValuePaths(path, value, paths, stateName, depth) {
662
+ paths.push({ path, kind: "data", typeHint: inferJsonTypeHint(value), stateName });
663
+ if (Array.isArray(value)) {
664
+ paths.push({ path: `${path}.*`, kind: "list", stateName });
665
+ paths.push({ path: `${path}.length`, kind: "data", typeHint: "number", stateName });
666
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
667
+ if (value.length > 0 && typeof value[0] === "object" && value[0] !== null && !Array.isArray(value[0])) {
668
+ const firstElement = value[0];
669
+ for (const [childKey, childValue] of Object.entries(firstElement)) {
670
+ pushJsonValuePaths(`${path}.*.${childKey}`, childValue, paths, stateName, depth + 1);
649
671
  }
650
- } else if (typeof value === "object" && value !== null) {
651
- collectJsonPaths(value, path, paths, stateName, depth + 1);
652
672
  }
673
+ } else if (typeof value === "object" && value !== null) {
674
+ collectJsonPaths(value, path, paths, stateName, depth + 1);
653
675
  }
654
676
  }
655
677
  function inferJsonTypeHint(value) {
@@ -820,21 +842,15 @@ function extractObjectContent(value) {
820
842
  if (start === -1) return "";
821
843
  return extractBracedContent(trimmed, scan, start);
822
844
  }
823
- function extractArrayElementProperties(value) {
845
+ function extractArrayElementDataProperties(value) {
824
846
  const trimmed = value.trim();
825
847
  if (!trimmed.startsWith("[")) return [];
826
848
  const scan = maskCommentsAndStrings(trimmed);
827
- const objectStart = scan.indexOf("{");
828
- if (objectStart === -1) return [];
829
- const objectContent = extractBracedContent(trimmed, scan, objectStart);
830
- const props = [];
831
- const allProps = parseTopLevelProperties(objectContent);
832
- for (const prop of allProps) {
833
- if (prop.kind === "data") {
834
- props.push({ name: prop.name, typeHint: prop.typeHint });
835
- }
836
- }
837
- return props;
849
+ let first = 1;
850
+ while (first < scan.length && /\s/.test(scan[first])) first++;
851
+ if (scan[first] !== "{") return [];
852
+ const objectContent = extractBracedContent(trimmed, scan, first);
853
+ return parseTopLevelProperties(objectContent).filter((prop) => prop.kind === "data");
838
854
  }
839
855
  function extractJsDocType(content, propIndex) {
840
856
  const before = content.slice(Math.max(0, propIndex - 200), propIndex);
@@ -966,25 +982,30 @@ function isInsideForTemplate(html, offset, bindAttrName = "data-wcs") {
966
982
  return false;
967
983
  }
968
984
  function getInnermostForPath(html, offset, bindAttrName = "data-wcs") {
985
+ const chain = getEnclosingForPaths(html, offset, bindAttrName);
986
+ return chain.length === 0 ? null : chain[chain.length - 1];
987
+ }
988
+ function getEnclosingForPaths(html, offset, bindAttrName = "data-wcs") {
989
+ return getEnclosingFors(html, offset, bindAttrName).map((entry) => entry.path);
990
+ }
991
+ function getEnclosingFors(html, offset, bindAttrName = "data-wcs") {
969
992
  const escaped = bindAttrName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
970
993
  const openRegex = new RegExp(
971
994
  `<template[^>]*${escaped}\\s*=\\s*["']\\s*for\\s*:\\s*([^"']+?)\\s*["']`,
972
995
  "gi"
973
996
  );
974
- let bestMatch = null;
975
- let bestPos = -1;
997
+ const enclosing = [];
976
998
  let match;
977
999
  while ((match = openRegex.exec(html)) !== null) {
978
1000
  if (match.index >= offset) break;
979
1001
  const tagEnd = html.indexOf(">", match.index);
980
1002
  if (tagEnd === -1 || tagEnd >= offset) continue;
981
1003
  const depth = getForTemplateDepthAt(html, match.index, offset, bindAttrName);
982
- if (depth > 0 && match.index > bestPos) {
983
- bestMatch = match[1].trim();
984
- bestPos = match.index;
1004
+ if (depth > 0) {
1005
+ enclosing.push({ path: match[1].trim(), anchor: match.index });
985
1006
  }
986
1007
  }
987
- return bestMatch;
1008
+ return enclosing;
988
1009
  }
989
1010
  function getForTemplateDepthAt(html, openPos, offset, bindAttrName) {
990
1011
  const tagEnd = html.indexOf(">", openPos);
@@ -1028,6 +1049,7 @@ var JA_EXPECTED_LABEL = {
1028
1049
  var ja = {
1029
1050
  spreadFilterNotAllowed: () => `\u30B9\u30D7\u30EC\u30C3\u30C9\u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306B\u30D5\u30A3\u30EB\u30BF\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`,
1030
1051
  spreadTargetRequired: () => `\u30B9\u30D7\u30EC\u30C3\u30C9\u306B\u306F\u30BF\u30FC\u30B2\u30C3\u30C8\u30D1\u30B9\u304C\u5FC5\u8981\u3067\u3059`,
1052
+ structuralMustBeSingle: (d) => `'${d}' \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0\u306F\u5358\u72EC\u3067\u6307\u5B9A\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\uFF08';' \u3067\u4ED6\u306E\u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0\u3068\u4F75\u8A18\u3067\u304D\u307E\u305B\u3093\u3002\u30E9\u30F3\u30BF\u30A4\u30E0\u306F\u8AAD\u307F\u8FBC\u307F\u6642\u306B throw \u3057\u307E\u3059\uFF09`,
1031
1053
  eventTokenUndeclared: (t) => `\u30A4\u30D9\u30F3\u30C8\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $eventTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
1032
1054
  commandRhsFormat: () => `command \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0\u306E\u53F3\u8FBA\u306B\u306F $command.<name>\uFF08$commandTokens \u3067\u5BA3\u8A00\uFF09\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`,
1033
1055
  commandTokenUndeclared: (t) => `\u30B3\u30DE\u30F3\u30C9\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $commandTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
@@ -1048,6 +1070,7 @@ var ja = {
1048
1070
  wcsTextInfo: (e) => `wcs-text \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0: ${e}`,
1049
1071
  moustacheFouc: (e) => `<template> \u5916\u306E {{ }} \u69CB\u6587\u306F FOUC\uFF08\u521D\u671F\u8868\u793A\u6642\u306B\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u6587\u5B57\u5217\u304C\u898B\u3048\u308B\uFF09\u306E\u539F\u56E0\u306B\u306A\u308A\u307E\u3059\u3002<!--@@:${e}--> \u307E\u305F\u306F\u30B3\u30E1\u30F3\u30C8\u69CB\u6587\u306E\u4F7F\u7528\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
1050
1072
  nestedAssign: (sp) => `\u30CD\u30B9\u30C8\u3055\u308C\u305F\u30D7\u30ED\u30D1\u30C6\u30A3\u3078\u306E\u4EE3\u5165\u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\u3002this["${sp}"] \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
1073
+ watchNotObject: () => `$watch \u306F\u300C\u30D1\u30B9 \u2192 \u30CF\u30F3\u30C9\u30E9\u95A2\u6570\u300D\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\uFF08\u3053\u306E\u5F62\u306F\u30E9\u30F3\u30BF\u30A4\u30E0\u304C\u8AAD\u307F\u8FBC\u307F\u6642\u306B throw \u3057\u307E\u3059\uFF09`,
1051
1074
  watchKeyCrossState: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F\u4ED6\u306E state \u3092\u6307\u3057\u3066\u3044\u307E\u3059\u3002@ \u4ED8\u304D\u306E\u8D8A\u5883 watch \u306F\u4F7F\u3048\u307E\u305B\u3093\uFF08\u81EA state \u306E\u30D1\u30B9\u306E\u307F\uFF09`,
1052
1075
  watchKeyReserved: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F "$" \u3067\u59CB\u3081\u3089\u308C\u307E\u305B\u3093\uFF08\u4E88\u7D04\u540D\u524D\u7A7A\u9593\uFF09`,
1053
1076
  watchKeyEmptySegment: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306B\u7A7A\u306E\u30D1\u30B9\u30BB\u30B0\u30E1\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059`,
@@ -1058,6 +1081,7 @@ var ja = {
1058
1081
  arrayIndexAssign: (sp) => `\u914D\u5217\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3078\u306E\u76F4\u63A5\u4EE3\u5165\u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\u3002this["${sp}"] \u306E\u3088\u3046\u306A\u30C9\u30C3\u30C8\u30D1\u30B9\u4EE3\u5165\u3001\u307E\u305F\u306F with() \u3068\u518D\u4EE3\u5165\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
1059
1082
  tagMemberUnknown: (prop, tag) => `"${prop}" \u306F <${tag}> \u306E wcBindable \u30E1\u30F3\u30D0\u30FC\u3067\u306F\u3042\u308A\u307E\u305B\u3093\uFF08\u672A\u77E5\u30E1\u30F3\u30D0\u30FC\u3078\u306E\u30D0\u30A4\u30F3\u30C9\u306F\u9ED9\u3063\u3066\u7121\u8996\u3055\u308C\u307E\u3059\uFF09`,
1060
1083
  tagCommandUnknown: (name, tag, declared) => `"${name}" \u306F <${tag}> \u306E command \u3067\u306F\u3042\u308A\u307E\u305B\u3093\uFF08\u5BA3\u8A00\u6E08\u307F: ${declared}\uFF09`,
1084
+ spreadNoBindable: (tag) => `'...'\uFF08spread\uFF09\u306F <${tag}> \u306B\u6709\u52B9\u306A wcBindable \u5BA3\u8A00\u304C\u5FC5\u8981\u3067\u3059 \u2014 \u3053\u306E\u30BF\u30B0\u306F\u5BA3\u8A00\u3092\u6301\u305F\u306A\u3044\u305F\u3081\u3001\u30E9\u30F3\u30BF\u30A4\u30E0\u306F\u30A8\u30E9\u30FC\u3092\u9001\u51FA\u3057\u307E\u3059`,
1061
1085
  tagEventTokenKeyUnknown: (name, tag, declared) => `eventToken \u306E\u30AD\u30FC "${name}" \u306F <${tag}> \u306E wcBindable \u30D7\u30ED\u30D1\u30C6\u30A3\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u751F DOM \u30A4\u30D9\u30F3\u30C8\u540D\u306F\u767A\u706B\u3057\u307E\u305B\u3093 \u2014 \u30D7\u30ED\u30D1\u30C6\u30A3\u540D\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u5BA3\u8A00\u6E08\u307F: ${declared}\uFF09`,
1062
1086
  didYouMean: (c) => `\u3002\u3082\u3057\u304B\u3057\u3066: "${c}"`,
1063
1087
  none: () => `\u306A\u3057`,
@@ -1075,6 +1099,7 @@ var EN_EXPECTED_LABEL = {
1075
1099
  var en = {
1076
1100
  spreadFilterNotAllowed: () => `Filters cannot be applied to a spread target`,
1077
1101
  spreadTargetRequired: () => `Spread requires a target path`,
1102
+ structuralMustBeSingle: (d) => `'${d}' must be the only binding in this attribute (it cannot be combined with ';'; the runtime throws at load time)`,
1078
1103
  eventTokenUndeclared: (t) => `Event token "${t}" is not declared in $eventTokens`,
1079
1104
  commandRhsFormat: () => `The right side of a command binding must be $command.<name> (declared in $commandTokens)`,
1080
1105
  commandTokenUndeclared: (t) => `Command token "${t}" is not declared in $commandTokens`,
@@ -1095,6 +1120,7 @@ var en = {
1095
1120
  wcsTextInfo: (e) => `wcs-text binding: ${e}`,
1096
1121
  moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
1097
1122
  nestedAssign: (sp) => `Assigning to a nested property does not trigger a reactive update. Use this["${sp}"] instead.`,
1123
+ watchNotObject: () => `$watch must be an object mapping state paths to handler functions (the runtime throws on this shape at load time)`,
1098
1124
  watchKeyCrossState: (k) => `$watch key "${k}" targets another state. Cross-state watching with @ is not supported (own paths only)`,
1099
1125
  watchKeyReserved: (k) => `$watch key "${k}" must not start with "$" (reserved namespace)`,
1100
1126
  watchKeyEmptySegment: (k) => `$watch key "${k}" has an empty path segment`,
@@ -1105,6 +1131,7 @@ var en = {
1105
1131
  arrayIndexAssign: (sp) => `Assigning directly to an array index does not trigger a reactive update. Use a dot-path assignment like this["${sp}"], or with() plus reassignment.`,
1106
1132
  tagMemberUnknown: (prop, tag) => `"${prop}" is not a wcBindable member of <${tag}> (bindings to unknown members are silently ignored)`,
1107
1133
  tagCommandUnknown: (name, tag, declared) => `"${name}" is not a command of <${tag}> (declared: ${declared})`,
1134
+ spreadNoBindable: (tag) => `'...' (spread) requires <${tag}> to expose a valid wcBindable declaration \u2014 this tag declares none, so the runtime raises an error`,
1108
1135
  tagEventTokenKeyUnknown: (name, tag, declared) => `eventToken key "${name}" is not a wcBindable property of <${tag}>. Raw DOM event names never fire \u2014 use the property name (declared: ${declared})`,
1109
1136
  didYouMean: (c) => `. Did you mean "${c}"?`,
1110
1137
  none: () => `none`,
@@ -1140,6 +1167,25 @@ function validateBindings(html, attrName, stateTagName = "wcs-state", locale, fi
1140
1167
  const filterNameSet = new Set(BUILTIN_FILTERS.map((f) => f.name));
1141
1168
  for (const attr of attrs) {
1142
1169
  const bindings = splitBindingExpressions(attr.value);
1170
+ const nonEmptyCount = bindings.filter((b) => b.trim().length > 0).length;
1171
+ if (nonEmptyCount > 1) {
1172
+ let scanPos = 0;
1173
+ for (const b of bindings) {
1174
+ const colon = b.indexOf(":");
1175
+ const prop = (colon === -1 ? b : b.slice(0, colon)).trim();
1176
+ if (STRUCTURAL_BINDING_TYPE_SET.has(prop)) {
1177
+ const leading = b.length - b.trimStart().length;
1178
+ diagnostics.push({
1179
+ code: WcsDiagnosticCode.TemplateSyntax,
1180
+ start: attr.valueStart + scanPos + leading,
1181
+ end: attr.valueStart + scanPos + b.trimEnd().length,
1182
+ message: msgs.structuralMustBeSingle(prop),
1183
+ severity: "error"
1184
+ });
1185
+ }
1186
+ scanPos += b.length + 1;
1187
+ }
1188
+ }
1143
1189
  let pos = 0;
1144
1190
  for (const binding of bindings) {
1145
1191
  const bindingStart = attr.valueStart + pos;
@@ -2032,6 +2078,7 @@ function isValidTemplatePath(path, pathSet, scopedPaths) {
2032
2078
  var BUILTIN_TAGS = {
2033
2079
  "wcs-accelerometer": {
2034
2080
  "package": "accelerometer",
2081
+ "hasWcBindable": true,
2035
2082
  "observedAttributes": [],
2036
2083
  "inputs": {
2037
2084
  "frequency": null
@@ -2050,6 +2097,7 @@ var BUILTIN_TAGS = {
2050
2097
  },
2051
2098
  "wcs-ambient-light-sensor": {
2052
2099
  "package": "ambient-light-sensor",
2100
+ "hasWcBindable": true,
2053
2101
  "observedAttributes": [],
2054
2102
  "inputs": {
2055
2103
  "frequency": null
@@ -2066,6 +2114,7 @@ var BUILTIN_TAGS = {
2066
2114
  },
2067
2115
  "wcs-audio": {
2068
2116
  "package": "audio",
2117
+ "hasWcBindable": true,
2069
2118
  "observedAttributes": [
2070
2119
  "volume",
2071
2120
  "limiter",
@@ -2098,6 +2147,7 @@ var BUILTIN_TAGS = {
2098
2147
  },
2099
2148
  "wcs-voice": {
2100
2149
  "package": "audio",
2150
+ "hasWcBindable": false,
2101
2151
  "observedAttributes": [
2102
2152
  "poly"
2103
2153
  ],
@@ -2107,6 +2157,7 @@ var BUILTIN_TAGS = {
2107
2157
  },
2108
2158
  "wcs-osc": {
2109
2159
  "package": "audio",
2160
+ "hasWcBindable": true,
2110
2161
  "observedAttributes": [
2111
2162
  "frequency",
2112
2163
  "detune",
@@ -2132,6 +2183,7 @@ var BUILTIN_TAGS = {
2132
2183
  },
2133
2184
  "wcs-noise": {
2134
2185
  "package": "audio",
2186
+ "hasWcBindable": true,
2135
2187
  "observedAttributes": [
2136
2188
  "id",
2137
2189
  "out",
@@ -2146,6 +2198,7 @@ var BUILTIN_TAGS = {
2146
2198
  },
2147
2199
  "wcs-biquad": {
2148
2200
  "package": "audio",
2201
+ "hasWcBindable": true,
2149
2202
  "observedAttributes": [
2150
2203
  "frequency",
2151
2204
  "q",
@@ -2171,6 +2224,7 @@ var BUILTIN_TAGS = {
2171
2224
  },
2172
2225
  "wcs-gain": {
2173
2226
  "package": "audio",
2227
+ "hasWcBindable": true,
2174
2228
  "observedAttributes": [
2175
2229
  "gain",
2176
2230
  "id",
@@ -2188,6 +2242,7 @@ var BUILTIN_TAGS = {
2188
2242
  },
2189
2243
  "wcs-delay": {
2190
2244
  "package": "audio",
2245
+ "hasWcBindable": true,
2191
2246
  "observedAttributes": [
2192
2247
  "time",
2193
2248
  "feedback",
@@ -2209,6 +2264,7 @@ var BUILTIN_TAGS = {
2209
2264
  },
2210
2265
  "wcs-shaper": {
2211
2266
  "package": "audio",
2267
+ "hasWcBindable": true,
2212
2268
  "observedAttributes": [
2213
2269
  "amount",
2214
2270
  "id",
@@ -2226,6 +2282,7 @@ var BUILTIN_TAGS = {
2226
2282
  },
2227
2283
  "wcs-env": {
2228
2284
  "package": "audio",
2285
+ "hasWcBindable": true,
2229
2286
  "observedAttributes": [
2230
2287
  "attack",
2231
2288
  "decay",
@@ -2251,6 +2308,7 @@ var BUILTIN_TAGS = {
2251
2308
  },
2252
2309
  "wcs-lfo": {
2253
2310
  "package": "audio",
2311
+ "hasWcBindable": true,
2254
2312
  "observedAttributes": [
2255
2313
  "rate",
2256
2314
  "depth",
@@ -2272,6 +2330,7 @@ var BUILTIN_TAGS = {
2272
2330
  },
2273
2331
  "wcs-analyser": {
2274
2332
  "package": "audio",
2333
+ "hasWcBindable": true,
2275
2334
  "observedAttributes": [
2276
2335
  "fft",
2277
2336
  "smoothing",
@@ -2295,6 +2354,7 @@ var BUILTIN_TAGS = {
2295
2354
  },
2296
2355
  "wcs-broadcast": {
2297
2356
  "package": "broadcast",
2357
+ "hasWcBindable": true,
2298
2358
  "observedAttributes": [
2299
2359
  "name"
2300
2360
  ],
@@ -2315,6 +2375,7 @@ var BUILTIN_TAGS = {
2315
2375
  },
2316
2376
  "wcs-camera": {
2317
2377
  "package": "camera",
2378
+ "hasWcBindable": true,
2318
2379
  "observedAttributes": [
2319
2380
  "facing-mode",
2320
2381
  "device-id",
@@ -2350,6 +2411,7 @@ var BUILTIN_TAGS = {
2350
2411
  },
2351
2412
  "wcs-recorder": {
2352
2413
  "package": "camera",
2414
+ "hasWcBindable": true,
2353
2415
  "observedAttributes": [],
2354
2416
  "inputs": {
2355
2417
  "mimeType": "mime-type",
@@ -2379,6 +2441,7 @@ var BUILTIN_TAGS = {
2379
2441
  },
2380
2442
  "wcs-clipboard": {
2381
2443
  "package": "clipboard",
2444
+ "hasWcBindable": true,
2382
2445
  "observedAttributes": [],
2383
2446
  "inputs": {
2384
2447
  "monitor": "monitor"
@@ -2407,6 +2470,7 @@ var BUILTIN_TAGS = {
2407
2470
  },
2408
2471
  "wcs-contacts": {
2409
2472
  "package": "contacts",
2473
+ "hasWcBindable": true,
2410
2474
  "observedAttributes": [],
2411
2475
  "inputs": {},
2412
2476
  "properties": [
@@ -2422,6 +2486,7 @@ var BUILTIN_TAGS = {
2422
2486
  },
2423
2487
  "wcs-credential": {
2424
2488
  "package": "credential",
2489
+ "hasWcBindable": true,
2425
2490
  "observedAttributes": [],
2426
2491
  "inputs": {},
2427
2492
  "properties": [
@@ -2438,6 +2503,7 @@ var BUILTIN_TAGS = {
2438
2503
  },
2439
2504
  "wcs-debounce": {
2440
2505
  "package": "debounce",
2506
+ "hasWcBindable": true,
2441
2507
  "observedAttributes": [],
2442
2508
  "inputs": {
2443
2509
  "source": null,
@@ -2459,6 +2525,7 @@ var BUILTIN_TAGS = {
2459
2525
  },
2460
2526
  "wcs-throttle": {
2461
2527
  "package": "debounce",
2528
+ "hasWcBindable": true,
2462
2529
  "observedAttributes": [],
2463
2530
  "inputs": {
2464
2531
  "source": null,
@@ -2480,6 +2547,7 @@ var BUILTIN_TAGS = {
2480
2547
  },
2481
2548
  "wcs-defined": {
2482
2549
  "package": "defined",
2550
+ "hasWcBindable": true,
2483
2551
  "observedAttributes": [],
2484
2552
  "inputs": {
2485
2553
  "tags": "tags",
@@ -2498,6 +2566,7 @@ var BUILTIN_TAGS = {
2498
2566
  },
2499
2567
  "wcs-eyedropper": {
2500
2568
  "package": "eyedropper",
2569
+ "hasWcBindable": true,
2501
2570
  "observedAttributes": [],
2502
2571
  "inputs": {},
2503
2572
  "properties": [
@@ -2514,6 +2583,7 @@ var BUILTIN_TAGS = {
2514
2583
  },
2515
2584
  "wcs-fetch": {
2516
2585
  "package": "fetch",
2586
+ "hasWcBindable": true,
2517
2587
  "observedAttributes": [
2518
2588
  "url"
2519
2589
  ],
@@ -2542,6 +2612,7 @@ var BUILTIN_TAGS = {
2542
2612
  },
2543
2613
  "wcs-fetch-header": {
2544
2614
  "package": "fetch",
2615
+ "hasWcBindable": false,
2545
2616
  "observedAttributes": [],
2546
2617
  "inputs": {},
2547
2618
  "properties": [],
@@ -2549,6 +2620,7 @@ var BUILTIN_TAGS = {
2549
2620
  },
2550
2621
  "wcs-fetch-body": {
2551
2622
  "package": "fetch",
2623
+ "hasWcBindable": false,
2552
2624
  "observedAttributes": [],
2553
2625
  "inputs": {},
2554
2626
  "properties": [],
@@ -2556,6 +2628,7 @@ var BUILTIN_TAGS = {
2556
2628
  },
2557
2629
  "wcs-infinite-scroll": {
2558
2630
  "package": "fetch",
2631
+ "hasWcBindable": false,
2559
2632
  "observedAttributes": [
2560
2633
  "target",
2561
2634
  "root",
@@ -2569,6 +2642,7 @@ var BUILTIN_TAGS = {
2569
2642
  },
2570
2643
  "wcs-fullscreen": {
2571
2644
  "package": "fullscreen",
2645
+ "hasWcBindable": true,
2572
2646
  "observedAttributes": [
2573
2647
  "target"
2574
2648
  ],
@@ -2587,6 +2661,7 @@ var BUILTIN_TAGS = {
2587
2661
  },
2588
2662
  "wcs-geo": {
2589
2663
  "package": "geolocation",
2664
+ "hasWcBindable": true,
2590
2665
  "observedAttributes": [],
2591
2666
  "inputs": {
2592
2667
  "highAccuracy": "high-accuracy",
@@ -2618,6 +2693,7 @@ var BUILTIN_TAGS = {
2618
2693
  },
2619
2694
  "wcs-gyroscope": {
2620
2695
  "package": "gyroscope",
2696
+ "hasWcBindable": true,
2621
2697
  "observedAttributes": [],
2622
2698
  "inputs": {
2623
2699
  "frequency": null
@@ -2636,6 +2712,7 @@ var BUILTIN_TAGS = {
2636
2712
  },
2637
2713
  "wcs-idle": {
2638
2714
  "package": "idle",
2715
+ "hasWcBindable": true,
2639
2716
  "observedAttributes": [],
2640
2717
  "inputs": {
2641
2718
  "threshold": "threshold"
@@ -2655,6 +2732,7 @@ var BUILTIN_TAGS = {
2655
2732
  },
2656
2733
  "wcs-intersect": {
2657
2734
  "package": "intersection",
2735
+ "hasWcBindable": true,
2658
2736
  "observedAttributes": [
2659
2737
  "target",
2660
2738
  "root",
@@ -2688,6 +2766,7 @@ var BUILTIN_TAGS = {
2688
2766
  },
2689
2767
  "wcs-magnetometer": {
2690
2768
  "package": "magnetometer",
2769
+ "hasWcBindable": true,
2691
2770
  "observedAttributes": [],
2692
2771
  "inputs": {
2693
2772
  "frequency": null
@@ -2706,6 +2785,7 @@ var BUILTIN_TAGS = {
2706
2785
  },
2707
2786
  "wcs-midi": {
2708
2787
  "package": "midi",
2788
+ "hasWcBindable": true,
2709
2789
  "observedAttributes": [
2710
2790
  "input",
2711
2791
  "output",
@@ -2743,6 +2823,7 @@ var BUILTIN_TAGS = {
2743
2823
  },
2744
2824
  "wcs-network": {
2745
2825
  "package": "network",
2826
+ "hasWcBindable": true,
2746
2827
  "observedAttributes": [],
2747
2828
  "inputs": {},
2748
2829
  "properties": [
@@ -2756,6 +2837,7 @@ var BUILTIN_TAGS = {
2756
2837
  },
2757
2838
  "wcs-notify": {
2758
2839
  "package": "notification",
2840
+ "hasWcBindable": true,
2759
2841
  "observedAttributes": [],
2760
2842
  "inputs": {
2761
2843
  "notice": null,
@@ -2792,6 +2874,7 @@ var BUILTIN_TAGS = {
2792
2874
  },
2793
2875
  "wcs-permission": {
2794
2876
  "package": "permission",
2877
+ "hasWcBindable": true,
2795
2878
  "observedAttributes": [],
2796
2879
  "inputs": {
2797
2880
  "name": "name",
@@ -2809,6 +2892,7 @@ var BUILTIN_TAGS = {
2809
2892
  },
2810
2893
  "wcs-pip": {
2811
2894
  "package": "picture-in-picture",
2895
+ "hasWcBindable": true,
2812
2896
  "observedAttributes": [
2813
2897
  "target"
2814
2898
  ],
@@ -2827,6 +2911,7 @@ var BUILTIN_TAGS = {
2827
2911
  },
2828
2912
  "wcs-pointer-lock": {
2829
2913
  "package": "pointer-lock",
2914
+ "hasWcBindable": true,
2830
2915
  "observedAttributes": [
2831
2916
  "target"
2832
2917
  ],
@@ -2845,6 +2930,7 @@ var BUILTIN_TAGS = {
2845
2930
  },
2846
2931
  "wcs-raf": {
2847
2932
  "package": "raf",
2933
+ "hasWcBindable": true,
2848
2934
  "observedAttributes": [],
2849
2935
  "inputs": {
2850
2936
  "once": "once",
@@ -2870,6 +2956,7 @@ var BUILTIN_TAGS = {
2870
2956
  },
2871
2957
  "wcs-resize": {
2872
2958
  "package": "resize",
2959
+ "hasWcBindable": true,
2873
2960
  "observedAttributes": [
2874
2961
  "target",
2875
2962
  "box",
@@ -2898,6 +2985,7 @@ var BUILTIN_TAGS = {
2898
2985
  },
2899
2986
  "wcs-screen-orientation": {
2900
2987
  "package": "screen-orientation",
2988
+ "hasWcBindable": true,
2901
2989
  "observedAttributes": [],
2902
2990
  "inputs": {},
2903
2991
  "properties": [
@@ -2915,6 +3003,7 @@ var BUILTIN_TAGS = {
2915
3003
  },
2916
3004
  "wcs-share": {
2917
3005
  "package": "share",
3006
+ "hasWcBindable": true,
2918
3007
  "observedAttributes": [],
2919
3008
  "inputs": {},
2920
3009
  "properties": [
@@ -2930,6 +3019,7 @@ var BUILTIN_TAGS = {
2930
3019
  },
2931
3020
  "wcs-speak": {
2932
3021
  "package": "speech",
3022
+ "hasWcBindable": true,
2933
3023
  "observedAttributes": [],
2934
3024
  "inputs": {
2935
3025
  "say": null,
@@ -2960,6 +3050,7 @@ var BUILTIN_TAGS = {
2960
3050
  },
2961
3051
  "wcs-listen": {
2962
3052
  "package": "speech",
3053
+ "hasWcBindable": true,
2963
3054
  "observedAttributes": [],
2964
3055
  "inputs": {
2965
3056
  "lang": "lang",
@@ -2988,6 +3079,7 @@ var BUILTIN_TAGS = {
2988
3079
  },
2989
3080
  "wcs-sse": {
2990
3081
  "package": "sse",
3082
+ "hasWcBindable": true,
2991
3083
  "observedAttributes": [
2992
3084
  "url"
2993
3085
  ],
@@ -3015,6 +3107,7 @@ var BUILTIN_TAGS = {
3015
3107
  },
3016
3108
  "wcs-storage": {
3017
3109
  "package": "storage",
3110
+ "hasWcBindable": true,
3018
3111
  "observedAttributes": [
3019
3112
  "key",
3020
3113
  "type"
@@ -3041,6 +3134,7 @@ var BUILTIN_TAGS = {
3041
3134
  },
3042
3135
  "wcs-tilt": {
3043
3136
  "package": "tilt",
3137
+ "hasWcBindable": true,
3044
3138
  "observedAttributes": [],
3045
3139
  "inputs": {},
3046
3140
  "properties": [
@@ -3060,6 +3154,7 @@ var BUILTIN_TAGS = {
3060
3154
  },
3061
3155
  "wcs-timer": {
3062
3156
  "package": "timer",
3157
+ "hasWcBindable": true,
3063
3158
  "observedAttributes": [
3064
3159
  "interval"
3065
3160
  ],
@@ -3087,6 +3182,7 @@ var BUILTIN_TAGS = {
3087
3182
  },
3088
3183
  "wcs-upload": {
3089
3184
  "package": "upload",
3185
+ "hasWcBindable": true,
3090
3186
  "observedAttributes": [
3091
3187
  "url"
3092
3188
  ],
@@ -3118,6 +3214,7 @@ var BUILTIN_TAGS = {
3118
3214
  },
3119
3215
  "wcs-wakelock": {
3120
3216
  "package": "wakelock",
3217
+ "hasWcBindable": true,
3121
3218
  "observedAttributes": [
3122
3219
  "active",
3123
3220
  "type"
@@ -3139,6 +3236,7 @@ var BUILTIN_TAGS = {
3139
3236
  },
3140
3237
  "wcs-ws": {
3141
3238
  "package": "websocket",
3239
+ "hasWcBindable": true,
3142
3240
  "observedAttributes": [
3143
3241
  "url"
3144
3242
  ],
@@ -3171,6 +3269,7 @@ var BUILTIN_TAGS = {
3171
3269
  },
3172
3270
  "wcs-worker": {
3173
3271
  "package": "worker",
3272
+ "hasWcBindable": true,
3174
3273
  "observedAttributes": [
3175
3274
  "src"
3176
3275
  ],
@@ -3224,10 +3323,28 @@ function validateIoNodes(html, bindAttribute = "data-wcs", stateTagName = "wcs-s
3224
3323
  const getPaths = () => statePaths ??= getStatePathsFromHtml(html, stateTagName, fileReader);
3225
3324
  for (const occ of occurrences) {
3226
3325
  const contract = BUILTIN_TAGS[occ.tagName];
3227
- if (contract.properties.length === 0 && contract.commands.length === 0 && Object.keys(contract.inputs).length === 0) continue;
3228
3326
  const bindAttr = extractAttributeValue(occ.attrsText, bindAttribute);
3229
3327
  if (!bindAttr) continue;
3230
3328
  const valueStart = occ.attrsStart + bindAttr.valueOffsetInAttrs;
3329
+ if (!contract.hasWcBindable) {
3330
+ let spreadExprOffset = 0;
3331
+ for (const expr of splitBindingExpressions(bindAttr.value)) {
3332
+ const exprStart = valueStart + spreadExprOffset;
3333
+ spreadExprOffset += expr.length + 1;
3334
+ if (parseBindingExpression(expr).property !== "...") continue;
3335
+ const start = exprStart + expr.indexOf("...");
3336
+ diagnostics.push({
3337
+ code: WcsDiagnosticCode.SpreadNoBindable,
3338
+ start,
3339
+ end: start + 3,
3340
+ severity: "error",
3341
+ tag: occ.tagName,
3342
+ message: msgs.spreadNoBindable(occ.tagName)
3343
+ });
3344
+ }
3345
+ continue;
3346
+ }
3347
+ if (contract.properties.length === 0 && contract.commands.length === 0 && Object.keys(contract.inputs).length === 0) continue;
3231
3348
  const hasManual = hasBooleanAttribute(occ.attrsText, "manual");
3232
3349
  let exprOffset = 0;
3233
3350
  for (const expr of splitBindingExpressions(bindAttr.value)) {
@@ -3525,6 +3642,16 @@ function validateWatchDeclarations(html, stateTagName = "wcs-state", locale) {
3525
3642
  const msgs = getMessages(locale);
3526
3643
  const out = [];
3527
3644
  for (const block of parseWcsScriptBlocks(html, stateTagName)) {
3645
+ const nonObject = findNonObjectWatch(block.content);
3646
+ if (nonObject !== null) {
3647
+ out.push({
3648
+ code: WcsDiagnosticCode.WatchDeclarationInvalid,
3649
+ start: block.contentStart + nonObject.start,
3650
+ end: block.contentStart + nonObject.end,
3651
+ message: msgs.watchNotObject(),
3652
+ severity: "error"
3653
+ });
3654
+ }
3528
3655
  const entries = analyzeWatchEntries(block.content);
3529
3656
  if (entries.length === 0) continue;
3530
3657
  const paths = analyzeStatePaths(block.content, block.stateName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/lint",
3
- "version": "1.28.0",
3
+ "version": "1.30.0",
4
4
  "description": "Static-contract validator CLI (wcs-validate) for wcstack data-wcs bindings and wcstack.manifest.json sidecars. Thin npm wrapper around the wcstack-intellisense validator core.",
5
5
  "type": "module",
6
6
  "bin": {