@wcstack/lint 1.26.0 → 1.28.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.
package/README.ja.md CHANGED
@@ -23,6 +23,8 @@ npx wcs-validate --errors-only src/**/*.html
23
23
 
24
24
  `.manifest.json` で終わるファイルは sidecar manifest として、それ以外は `data-wcs` バインディングを含む HTML として検査されます。
25
25
 
26
+ `<wcs-state src="...">` で参照される外部 state(`.json` / `.js` / `.ts`)は HTML ファイルからの相対パスで解決され、パス検証の対象になります。URL・絶対パスは読み込まず、読めないファイルはスキップされ、解決できないパスは warning のままです。なお外部 state が**解決できた**ページには通常の検証面が全て適用されるため、error severity の検出(例: 配列でない値への `for:`)が「これまでパス未解決で沈黙していたビルド」を新たに落とすことがあります。
27
+
26
28
  ```
27
29
  wcs-validate [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] [--errors-only] <file> [<file> ...]
28
30
  ```
package/README.md CHANGED
@@ -23,6 +23,8 @@ npx wcs-validate --errors-only src/**/*.html
23
23
 
24
24
  Files ending in `.manifest.json` are validated as sidecar manifests; everything else is validated as HTML with `data-wcs` bindings.
25
25
 
26
+ External state referenced via `<wcs-state src="...">` (`.json` / `.js` / `.ts`) is resolved relative to the HTML file and included in path validation. URLs and absolute paths are never read, unreadable files are skipped, and unresolved paths stay warnings. Note that once external state *does* resolve, the full validation surface applies to those pages — error-severity findings (e.g. `for:` bound to a non-array) can fail a build that was previously silent because the paths could not be checked.
27
+
26
28
  ```
27
29
  wcs-validate [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] [--errors-only] <file> [<file> ...]
28
30
  ```
package/dist/cli.cjs CHANGED
@@ -21,12 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/cli.ts
22
22
  var cli_exports = {};
23
23
  __export(cli_exports, {
24
+ createFileReader: () => createFileReader,
24
25
  main: () => main,
25
26
  parseArgs: () => parseArgs,
26
27
  resolveCliLocale: () => resolveCliLocale
27
28
  });
28
29
  module.exports = __toCommonJS(cli_exports);
29
30
  var import_node_fs = require("node:fs");
31
+ var import_node_path = require("node:path");
30
32
 
31
33
  // src/core/offsetToPosition.ts
32
34
  function createPositionMapper(text) {
@@ -89,6 +91,13 @@ var WcsDiagnosticCode = {
89
91
  TokenUndeclared: "wcs/token-undeclared",
90
92
  TokenMisconfigured: "wcs/token-misconfigured",
91
93
  NestedAssign: "wcs/nested-assign",
94
+ // --- <wcs-state> script: $watch declaration ---
95
+ // ランタイム(watch/processWatchDeclaration.ts)が raiseError で落とす宣言。
96
+ // 越境 `@` / `$` 始まり / 空キー・空セグメント / 明らかな非関数ハンドラ。
97
+ WatchDeclarationInvalid: "wcs/watch-declaration-invalid",
98
+ // `$watch` のキーが状態定義に存在しない。バインディング側と違い黙って発火しない
99
+ // だけなので気づけない。severity は binding-path-missing に揃える(warning)。
100
+ WatchPathMissing: "wcs/watch-path-missing",
92
101
  TypeAnnotation: "wcs/type-annotation",
93
102
  TemplateSyntax: "wcs/template-syntax",
94
103
  // --- <wcs-state> script: array reactivity hazards ---
@@ -134,6 +143,8 @@ var builtinFilterMeta = {
134
143
  mul: { description: "\u4E57\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
135
144
  div: { description: "\u9664\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
136
145
  mod: { description: "\u5270\u4F59", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
146
+ abs: { description: "\u7D76\u5BFE\u5024", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
147
+ clamp: { description: "\u7BC4\u56F2\u5185\u306B\u4E38\u3081\u308B (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
137
148
  // 数値フォーマット
138
149
  fix: { description: "\u56FA\u5B9A\u5C0F\u6570\u70B9\u8868\u8A18", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
139
150
  locale: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u3067\u6570\u5024\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
@@ -147,6 +158,8 @@ var builtinFilterMeta = {
147
158
  pad: { description: "\u30D1\u30C7\u30A3\u30F3\u30B0 (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
148
159
  rep: { description: "\u7E70\u308A\u8FD4\u3057 (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
149
160
  rev: { description: "\u6587\u5B57\u9806\u3092\u53CD\u8EE2", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
161
+ truncate: { description: "\u5207\u308A\u8A70\u3081\u3066\u7701\u7565\u8A18\u53F7 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
162
+ join: { description: "\u914D\u5217\u3092\u9023\u7D50 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
150
163
  // 数値パース・丸め
151
164
  int: { description: "\u6574\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
152
165
  float: { description: "\u6D6E\u52D5\u5C0F\u6570\u70B9\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
@@ -154,11 +167,15 @@ var builtinFilterMeta = {
154
167
  floor: { description: "\u5207\u308A\u4E0B\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
155
168
  ceil: { description: "\u5207\u308A\u4E0A\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
156
169
  percent: { description: "\u30D1\u30FC\u30BB\u30F3\u30C6\u30FC\u30B8\u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
170
+ // number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
171
+ // それらは既に string を返すため(builtinFilters.ts の unit を参照)
172
+ unit: { description: "\u5358\u4F4D\uFF08\u63A5\u5C3E\u8F9E\uFF09\u3092\u4ED8\u52A0", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
157
173
  // 日付・時刻
158
174
  date: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u4ED8", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
159
175
  time: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u6642\u523B", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
160
176
  datetime: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u6642", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
161
177
  ymd: { description: "YYYY-MM-DD \u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
178
+ hms: { description: "HH:MM:SS \u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
162
179
  // 真偽値・変換
163
180
  falsy: { description: "\u507D\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
164
181
  truthy: { description: "\u771F\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
@@ -175,9 +192,24 @@ var STRUCTURAL_BINDING_TYPE_SET = /* @__PURE__ */ new Set([
175
192
  "for"
176
193
  ]);
177
194
  var MAX_WILDCARD_DEPTH = 128;
195
+ var MODIFIER_PREVENT = "prevent";
196
+ var MODIFIER_STOP = "stop";
197
+ var MODIFIER_READONLY = "ro";
198
+ var MODIFIER_FLAGS = Object.freeze([
199
+ MODIFIER_PREVENT,
200
+ MODIFIER_STOP,
201
+ MODIFIER_READONLY
202
+ ]);
203
+ var MODIFIER_KEY_INIT = "init";
204
+ var MODIFIER_KEY_SYNC = "sync";
205
+ var MODIFIER_KEYS = Object.freeze([
206
+ MODIFIER_KEY_INIT,
207
+ MODIFIER_KEY_SYNC
208
+ ]);
209
+ var INDEX_PARAM_PREFIX = "$";
178
210
  var tmpIndexByIndexName = {};
179
211
  for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
180
- tmpIndexByIndexName[`$${i + 1}`] = i;
212
+ tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
181
213
  }
182
214
  Object.freeze(tmpIndexByIndexName);
183
215
 
@@ -419,6 +451,7 @@ var RESERVED_STREAMS_KEY = "$streams";
419
451
  var RESERVED_COMMAND_TOKENS_KEY = "$commandTokens";
420
452
  var RESERVED_EVENT_TOKENS_KEY = "$eventTokens";
421
453
  var RESERVED_LIST_KEYS_KEY = "$listKeys";
454
+ var RESERVED_WATCH_KEY = "$watch";
422
455
  function analyzeStatePaths(scriptContent, stateName = "default") {
423
456
  const objectContent = extractDefaultExportObject(scriptContent);
424
457
  if (!objectContent) return [];
@@ -452,6 +485,36 @@ function analyzeStatePaths(scriptContent, stateName = "default") {
452
485
  }
453
486
  return paths;
454
487
  }
488
+ function analyzeWatchEntries(scriptContent) {
489
+ const root = locateDefaultExportObject(scriptContent);
490
+ if (!root) return [];
491
+ const watchProp = parseTopLevelProperties(root.content).find((p) => p.name === RESERVED_WATCH_KEY);
492
+ if (!watchProp || watchProp.kind !== "data" || !watchProp.value || !isObjectLiteral(watchProp.value) || watchProp.valueStart === void 0) {
493
+ return [];
494
+ }
495
+ const leading = watchProp.value.length - watchProp.value.trimStart().length;
496
+ const innerStart = root.start + watchProp.valueStart + leading + 1;
497
+ const entries = [];
498
+ for (const entry of parseTopLevelProperties(extractObjectContent(watchProp.value))) {
499
+ if (entry.nameStart === void 0 || entry.nameEnd === void 0) continue;
500
+ entries.push({
501
+ key: entry.name,
502
+ start: innerStart + entry.nameStart,
503
+ end: innerStart + entry.nameEnd,
504
+ // メソッド短縮記法は関数。data は値リテラルの形で判定し、識別子参照は疑わない。
505
+ definitelyNotFunction: entry.kind === "data" && isNonFunctionLiteral(entry.value)
506
+ });
507
+ }
508
+ return entries;
509
+ }
510
+ function isNonFunctionLiteral(value) {
511
+ if (value === void 0) return false;
512
+ const trimmed = value.trim();
513
+ if (trimmed.length === 0) return false;
514
+ const scan = maskCommentsAndStrings(trimmed);
515
+ if (/^(?:async\s+)?function\b/.test(trimmed) || scan.includes("=>")) return false;
516
+ return /^["'`]/.test(trimmed) || /^-?\d/.test(trimmed) || /^(?:true|false|null|undefined)\b/.test(trimmed) || trimmed.startsWith("[") || trimmed.startsWith("{");
517
+ }
455
518
  function collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys, stateName) {
456
519
  if (prop.name === RESERVED_STREAMS_KEY && prop.kind === "data" && prop.value && isObjectLiteral(prop.value)) {
457
520
  const entries = parseTopLevelProperties(extractObjectContent(prop.value));
@@ -598,23 +661,29 @@ function inferJsonTypeHint(value) {
598
661
  if (typeof value === "object") return "object";
599
662
  return void 0;
600
663
  }
601
- function extractDefaultExportObject(script) {
664
+ function locateDefaultExportObject(script) {
602
665
  const scan = maskCommentsAndStrings(script);
603
666
  const match = scan.match(/export\s+default\s+(?:defineState\s*\(\s*)?(\{)/);
604
667
  if (!match) return null;
605
- const startIndex = scan.indexOf(match[1], match.index);
606
- return extractBracedContent(script, scan, startIndex);
668
+ const braceIndex = scan.indexOf(match[1], match.index);
669
+ return { content: extractBracedContent(script, scan, braceIndex), start: braceIndex + 1 };
670
+ }
671
+ function extractDefaultExportObject(script) {
672
+ return locateDefaultExportObject(script)?.content ?? null;
607
673
  }
608
674
  function parseTopLevelProperties(objectContent) {
609
675
  const props = [];
610
676
  const scan = maskCommentsAndStrings(objectContent);
611
- const regex = /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?([$\w]+)\s*\([^)]*\)\s*\{)|(?:(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*:\s*)/gd;
677
+ const regex = /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*:\s*)/gd;
612
678
  let match;
613
679
  while ((match = regex.exec(scan)) !== null) {
614
680
  const indices = match.indices;
681
+ let nameSpan;
615
682
  const nameAt = (group) => {
616
683
  const span = indices[group];
617
- return span ? objectContent.slice(span[0], span[1]) : void 0;
684
+ if (!span) return void 0;
685
+ nameSpan = [span[0], span[1]];
686
+ return objectContent.slice(span[0], span[1]);
618
687
  };
619
688
  const skipBody = () => {
620
689
  const braceStart = match.index + match[0].length - 1;
@@ -623,23 +692,31 @@ function parseTopLevelProperties(objectContent) {
623
692
  };
624
693
  const accessorName = nameAt(1) ?? nameAt(2) ?? nameAt(3);
625
694
  if (accessorName) {
626
- props.push({ name: accessorName, kind: "getter" });
695
+ props.push({ name: accessorName, kind: "getter", nameStart: nameSpan[0], nameEnd: nameSpan[1] });
627
696
  skipBody();
628
697
  continue;
629
698
  }
630
- const methodName = nameAt(4);
699
+ const methodName = nameAt(4) ?? nameAt(5) ?? nameAt(6);
631
700
  if (methodName) {
632
- props.push({ name: methodName, kind: "method" });
701
+ props.push({ name: methodName, kind: "method", nameStart: nameSpan[0], nameEnd: nameSpan[1] });
633
702
  skipBody();
634
703
  continue;
635
704
  }
636
- const propName = nameAt(5) ?? nameAt(6) ?? nameAt(7);
705
+ const propName = nameAt(7) ?? nameAt(8) ?? nameAt(9);
637
706
  if (propName) {
638
707
  const valueStartIndex = match.index + match[0].length;
639
708
  const value = extractFullValue(objectContent, scan, valueStartIndex);
640
709
  const jsdocType = extractJsDocType(objectContent, match.index);
641
710
  const typeHint = jsdocType ?? inferTypeHint(value);
642
- props.push({ name: propName, kind: "data", value, typeHint });
711
+ props.push({
712
+ name: propName,
713
+ kind: "data",
714
+ value,
715
+ typeHint,
716
+ nameStart: nameSpan[0],
717
+ nameEnd: nameSpan[1],
718
+ valueStart: valueStartIndex
719
+ });
643
720
  regex.lastIndex = valueStartIndex + value.length;
644
721
  }
645
722
  }
@@ -971,6 +1048,11 @@ var ja = {
971
1048
  wcsTextInfo: (e) => `wcs-text \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0: ${e}`,
972
1049
  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`,
973
1050
  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`,
1051
+ 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
+ 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
+ watchKeyEmptySegment: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306B\u7A7A\u306E\u30D1\u30B9\u30BB\u30B0\u30E1\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059`,
1054
+ watchHandlerNotFunction: (k) => `$watch \u306E\u30A8\u30F3\u30C8\u30EA "${k}" \u306E\u5024\u306F\u95A2\u6570\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059`,
1055
+ watchPathMissing: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F\u72B6\u614B\u5B9A\u7FA9\u306B\u5B58\u5728\u3057\u307E\u305B\u3093\uFF08\u4E00\u5EA6\u3082\u767A\u706B\u3057\u307E\u305B\u3093\uFF09`,
974
1056
  typeAnnotationIncompatible: (vt, rt) => `\u578B "${vt}" \u306F @type {${rt}} \u3068\u4E92\u63DB\u6027\u304C\u3042\u308A\u307E\u305B\u3093`,
975
1057
  arrayMutation: (m, alt) => `\u914D\u5217\u306E\u7834\u58CA\u7684\u30E1\u30BD\u30C3\u30C9 "${m}" \u306F\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u66F4\u65B0\u3092\u30C8\u30EA\u30AC\u30FC\u3057\u307E\u305B\u3093\uFF08\u540C\u4E00\u53C2\u7167\u306E\u81EA\u5DF1\u518D\u4EE3\u5165\u3067\u3082\u8981\u7D20\u306E\u8FFD\u52A0\u30FB\u524A\u9664\u306F\u53CD\u6620\u3055\u308C\u307E\u305B\u3093\uFF09\u3002\u975E\u7834\u58CA\u30E1\u30BD\u30C3\u30C9\u3068\u518D\u4EE3\u5165\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4F8B: ${alt}\uFF09\u3002`,
976
1058
  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`,
@@ -1013,6 +1095,11 @@ var en = {
1013
1095
  wcsTextInfo: (e) => `wcs-text binding: ${e}`,
1014
1096
  moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
1015
1097
  nestedAssign: (sp) => `Assigning to a nested property does not trigger a reactive update. Use this["${sp}"] instead.`,
1098
+ watchKeyCrossState: (k) => `$watch key "${k}" targets another state. Cross-state watching with @ is not supported (own paths only)`,
1099
+ watchKeyReserved: (k) => `$watch key "${k}" must not start with "$" (reserved namespace)`,
1100
+ watchKeyEmptySegment: (k) => `$watch key "${k}" has an empty path segment`,
1101
+ watchHandlerNotFunction: (k) => `The value of $watch entry "${k}" must be a function`,
1102
+ watchPathMissing: (k) => `$watch key "${k}" does not exist in the state definition (it will never fire)`,
1016
1103
  typeAnnotationIncompatible: (vt, rt) => `Type "${vt}" is not compatible with @type {${rt}}`,
1017
1104
  arrayMutation: (m, alt) => `Destructive array method "${m}" does not trigger a reactive update (re-assigning the same reference does not reflect added/removed elements either). Use a non-destructive method with reassignment (e.g. ${alt}).`,
1018
1105
  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.`,
@@ -1034,10 +1121,10 @@ function getMessages(locale) {
1034
1121
 
1035
1122
  // src/service/bindingValidator.ts
1036
1123
  var filterMap = new Map(BUILTIN_FILTERS.map((f) => [f.name, f]));
1037
- function validateBindings(html, attrName, stateTagName = "wcs-state", locale) {
1124
+ function validateBindings(html, attrName, stateTagName = "wcs-state", locale, fileReader) {
1038
1125
  const diagnostics = [];
1039
1126
  const msgs = getMessages(locale);
1040
- const statePaths = getStatePathsFromHtml(html, stateTagName);
1127
+ const statePaths = getStatePathsFromHtml(html, stateTagName, fileReader);
1041
1128
  const pathsByState = /* @__PURE__ */ new Map();
1042
1129
  for (const p of statePaths) {
1043
1130
  const list = pathsByState.get(p.stateName) ?? [];
@@ -1828,10 +1915,10 @@ function isInsideTag(html, offset, tagName) {
1828
1915
  }
1829
1916
 
1830
1917
  // src/service/templateSyntaxValidator.ts
1831
- function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale) {
1918
+ function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale, fileReader) {
1832
1919
  const diagnostics = [];
1833
1920
  const msgs = getMessages(locale);
1834
- const allPaths = getStatePathsFromHtml(html, stateTagName);
1921
+ const allPaths = getStatePathsFromHtml(html, stateTagName, fileReader);
1835
1922
  if (allPaths.length === 0) return diagnostics;
1836
1923
  const defaultPaths = allPaths.filter((p) => p.stateName === "default");
1837
1924
  const pathSet = new Set(defaultPaths.map((p) => p.path));
@@ -1945,6 +2032,7 @@ function isValidTemplatePath(path, pathSet, scopedPaths) {
1945
2032
  var BUILTIN_TAGS = {
1946
2033
  "wcs-accelerometer": {
1947
2034
  "package": "accelerometer",
2035
+ "observedAttributes": [],
1948
2036
  "inputs": {
1949
2037
  "frequency": null
1950
2038
  },
@@ -1962,6 +2050,7 @@ var BUILTIN_TAGS = {
1962
2050
  },
1963
2051
  "wcs-ambient-light-sensor": {
1964
2052
  "package": "ambient-light-sensor",
2053
+ "observedAttributes": [],
1965
2054
  "inputs": {
1966
2055
  "frequency": null
1967
2056
  },
@@ -1975,8 +2064,240 @@ var BUILTIN_TAGS = {
1975
2064
  "stop"
1976
2065
  ]
1977
2066
  },
2067
+ "wcs-audio": {
2068
+ "package": "audio",
2069
+ "observedAttributes": [
2070
+ "volume",
2071
+ "limiter",
2072
+ "resume-on-gesture"
2073
+ ],
2074
+ "inputs": {
2075
+ "volume": "volume",
2076
+ "limiter": "limiter",
2077
+ "resumeOnGesture": "resume-on-gesture"
2078
+ },
2079
+ "properties": [
2080
+ "state",
2081
+ "running",
2082
+ "suspended",
2083
+ "unsupported",
2084
+ "voices",
2085
+ "noteOn",
2086
+ "noteOff",
2087
+ "warnings",
2088
+ "error",
2089
+ "errorInfo"
2090
+ ],
2091
+ "commands": [
2092
+ "resume",
2093
+ "suspend",
2094
+ "noteOn",
2095
+ "noteOff",
2096
+ "allNotesOff"
2097
+ ]
2098
+ },
2099
+ "wcs-voice": {
2100
+ "package": "audio",
2101
+ "observedAttributes": [
2102
+ "poly"
2103
+ ],
2104
+ "inputs": {},
2105
+ "properties": [],
2106
+ "commands": []
2107
+ },
2108
+ "wcs-osc": {
2109
+ "package": "audio",
2110
+ "observedAttributes": [
2111
+ "frequency",
2112
+ "detune",
2113
+ "type",
2114
+ "glide",
2115
+ "transpose",
2116
+ "id",
2117
+ "out",
2118
+ "param",
2119
+ "note",
2120
+ "master",
2121
+ "poly"
2122
+ ],
2123
+ "inputs": {
2124
+ "frequency": "frequency",
2125
+ "detune": "detune",
2126
+ "type": "type",
2127
+ "glide": "glide",
2128
+ "transpose": "transpose"
2129
+ },
2130
+ "properties": [],
2131
+ "commands": []
2132
+ },
2133
+ "wcs-noise": {
2134
+ "package": "audio",
2135
+ "observedAttributes": [
2136
+ "id",
2137
+ "out",
2138
+ "param",
2139
+ "note",
2140
+ "master",
2141
+ "poly"
2142
+ ],
2143
+ "inputs": {},
2144
+ "properties": [],
2145
+ "commands": []
2146
+ },
2147
+ "wcs-biquad": {
2148
+ "package": "audio",
2149
+ "observedAttributes": [
2150
+ "frequency",
2151
+ "q",
2152
+ "gain",
2153
+ "detune",
2154
+ "type",
2155
+ "id",
2156
+ "out",
2157
+ "param",
2158
+ "note",
2159
+ "master",
2160
+ "poly"
2161
+ ],
2162
+ "inputs": {
2163
+ "frequency": "frequency",
2164
+ "q": "q",
2165
+ "gain": "gain",
2166
+ "detune": "detune",
2167
+ "type": "type"
2168
+ },
2169
+ "properties": [],
2170
+ "commands": []
2171
+ },
2172
+ "wcs-gain": {
2173
+ "package": "audio",
2174
+ "observedAttributes": [
2175
+ "gain",
2176
+ "id",
2177
+ "out",
2178
+ "param",
2179
+ "note",
2180
+ "master",
2181
+ "poly"
2182
+ ],
2183
+ "inputs": {
2184
+ "gain": "gain"
2185
+ },
2186
+ "properties": [],
2187
+ "commands": []
2188
+ },
2189
+ "wcs-delay": {
2190
+ "package": "audio",
2191
+ "observedAttributes": [
2192
+ "time",
2193
+ "feedback",
2194
+ "mix",
2195
+ "id",
2196
+ "out",
2197
+ "param",
2198
+ "note",
2199
+ "master",
2200
+ "poly"
2201
+ ],
2202
+ "inputs": {
2203
+ "time": "time",
2204
+ "feedback": "feedback",
2205
+ "mix": "mix"
2206
+ },
2207
+ "properties": [],
2208
+ "commands": []
2209
+ },
2210
+ "wcs-shaper": {
2211
+ "package": "audio",
2212
+ "observedAttributes": [
2213
+ "amount",
2214
+ "id",
2215
+ "out",
2216
+ "param",
2217
+ "note",
2218
+ "master",
2219
+ "poly"
2220
+ ],
2221
+ "inputs": {
2222
+ "amount": "amount"
2223
+ },
2224
+ "properties": [],
2225
+ "commands": []
2226
+ },
2227
+ "wcs-env": {
2228
+ "package": "audio",
2229
+ "observedAttributes": [
2230
+ "attack",
2231
+ "decay",
2232
+ "sustain",
2233
+ "release",
2234
+ "depth",
2235
+ "id",
2236
+ "out",
2237
+ "param",
2238
+ "note",
2239
+ "master",
2240
+ "poly"
2241
+ ],
2242
+ "inputs": {
2243
+ "attack": "attack",
2244
+ "decay": "decay",
2245
+ "sustain": "sustain",
2246
+ "release": "release",
2247
+ "depth": "depth"
2248
+ },
2249
+ "properties": [],
2250
+ "commands": []
2251
+ },
2252
+ "wcs-lfo": {
2253
+ "package": "audio",
2254
+ "observedAttributes": [
2255
+ "rate",
2256
+ "depth",
2257
+ "type",
2258
+ "id",
2259
+ "out",
2260
+ "param",
2261
+ "note",
2262
+ "master",
2263
+ "poly"
2264
+ ],
2265
+ "inputs": {
2266
+ "rate": "rate",
2267
+ "depth": "depth",
2268
+ "type": "type"
2269
+ },
2270
+ "properties": [],
2271
+ "commands": []
2272
+ },
2273
+ "wcs-analyser": {
2274
+ "package": "audio",
2275
+ "observedAttributes": [
2276
+ "fft",
2277
+ "smoothing",
2278
+ "id",
2279
+ "out",
2280
+ "param",
2281
+ "note",
2282
+ "master",
2283
+ "poly"
2284
+ ],
2285
+ "inputs": {
2286
+ "fft": "fft",
2287
+ "smoothing": "smoothing"
2288
+ },
2289
+ "properties": [
2290
+ "frame"
2291
+ ],
2292
+ "commands": [
2293
+ "sample"
2294
+ ]
2295
+ },
1978
2296
  "wcs-broadcast": {
1979
2297
  "package": "broadcast",
2298
+ "observedAttributes": [
2299
+ "name"
2300
+ ],
1980
2301
  "inputs": {
1981
2302
  "name": "name",
1982
2303
  "manual": "manual"
@@ -1994,6 +2315,13 @@ var BUILTIN_TAGS = {
1994
2315
  },
1995
2316
  "wcs-camera": {
1996
2317
  "package": "camera",
2318
+ "observedAttributes": [
2319
+ "facing-mode",
2320
+ "device-id",
2321
+ "audio",
2322
+ "width",
2323
+ "height"
2324
+ ],
1997
2325
  "inputs": {
1998
2326
  "audio": "audio",
1999
2327
  "facingMode": "facing-mode",
@@ -2022,6 +2350,7 @@ var BUILTIN_TAGS = {
2022
2350
  },
2023
2351
  "wcs-recorder": {
2024
2352
  "package": "camera",
2353
+ "observedAttributes": [],
2025
2354
  "inputs": {
2026
2355
  "mimeType": "mime-type",
2027
2356
  "timeslice": "timeslice",
@@ -2050,6 +2379,7 @@ var BUILTIN_TAGS = {
2050
2379
  },
2051
2380
  "wcs-clipboard": {
2052
2381
  "package": "clipboard",
2382
+ "observedAttributes": [],
2053
2383
  "inputs": {
2054
2384
  "monitor": "monitor"
2055
2385
  },
@@ -2077,6 +2407,7 @@ var BUILTIN_TAGS = {
2077
2407
  },
2078
2408
  "wcs-contacts": {
2079
2409
  "package": "contacts",
2410
+ "observedAttributes": [],
2080
2411
  "inputs": {},
2081
2412
  "properties": [
2082
2413
  "value",
@@ -2091,6 +2422,7 @@ var BUILTIN_TAGS = {
2091
2422
  },
2092
2423
  "wcs-credential": {
2093
2424
  "package": "credential",
2425
+ "observedAttributes": [],
2094
2426
  "inputs": {},
2095
2427
  "properties": [
2096
2428
  "value",
@@ -2106,6 +2438,7 @@ var BUILTIN_TAGS = {
2106
2438
  },
2107
2439
  "wcs-debounce": {
2108
2440
  "package": "debounce",
2441
+ "observedAttributes": [],
2109
2442
  "inputs": {
2110
2443
  "source": null,
2111
2444
  "wait": "wait",
@@ -2126,6 +2459,7 @@ var BUILTIN_TAGS = {
2126
2459
  },
2127
2460
  "wcs-throttle": {
2128
2461
  "package": "debounce",
2462
+ "observedAttributes": [],
2129
2463
  "inputs": {
2130
2464
  "source": null,
2131
2465
  "wait": "wait",
@@ -2146,6 +2480,7 @@ var BUILTIN_TAGS = {
2146
2480
  },
2147
2481
  "wcs-defined": {
2148
2482
  "package": "defined",
2483
+ "observedAttributes": [],
2149
2484
  "inputs": {
2150
2485
  "tags": "tags",
2151
2486
  "mode": "mode",
@@ -2163,6 +2498,7 @@ var BUILTIN_TAGS = {
2163
2498
  },
2164
2499
  "wcs-eyedropper": {
2165
2500
  "package": "eyedropper",
2501
+ "observedAttributes": [],
2166
2502
  "inputs": {},
2167
2503
  "properties": [
2168
2504
  "value",
@@ -2178,6 +2514,9 @@ var BUILTIN_TAGS = {
2178
2514
  },
2179
2515
  "wcs-fetch": {
2180
2516
  "package": "fetch",
2517
+ "observedAttributes": [
2518
+ "url"
2519
+ ],
2181
2520
  "inputs": {
2182
2521
  "url": null,
2183
2522
  "method": null,
@@ -2203,24 +2542,36 @@ var BUILTIN_TAGS = {
2203
2542
  },
2204
2543
  "wcs-fetch-header": {
2205
2544
  "package": "fetch",
2545
+ "observedAttributes": [],
2206
2546
  "inputs": {},
2207
2547
  "properties": [],
2208
2548
  "commands": []
2209
2549
  },
2210
2550
  "wcs-fetch-body": {
2211
2551
  "package": "fetch",
2552
+ "observedAttributes": [],
2212
2553
  "inputs": {},
2213
2554
  "properties": [],
2214
2555
  "commands": []
2215
2556
  },
2216
2557
  "wcs-infinite-scroll": {
2217
2558
  "package": "fetch",
2559
+ "observedAttributes": [
2560
+ "target",
2561
+ "root",
2562
+ "root-margin",
2563
+ "threshold",
2564
+ "disabled"
2565
+ ],
2218
2566
  "inputs": {},
2219
2567
  "properties": [],
2220
2568
  "commands": []
2221
2569
  },
2222
2570
  "wcs-fullscreen": {
2223
2571
  "package": "fullscreen",
2572
+ "observedAttributes": [
2573
+ "target"
2574
+ ],
2224
2575
  "inputs": {
2225
2576
  "target": "target"
2226
2577
  },
@@ -2236,6 +2587,7 @@ var BUILTIN_TAGS = {
2236
2587
  },
2237
2588
  "wcs-geo": {
2238
2589
  "package": "geolocation",
2590
+ "observedAttributes": [],
2239
2591
  "inputs": {
2240
2592
  "highAccuracy": "high-accuracy",
2241
2593
  "timeout": "timeout",
@@ -2266,6 +2618,7 @@ var BUILTIN_TAGS = {
2266
2618
  },
2267
2619
  "wcs-gyroscope": {
2268
2620
  "package": "gyroscope",
2621
+ "observedAttributes": [],
2269
2622
  "inputs": {
2270
2623
  "frequency": null
2271
2624
  },
@@ -2283,6 +2636,7 @@ var BUILTIN_TAGS = {
2283
2636
  },
2284
2637
  "wcs-idle": {
2285
2638
  "package": "idle",
2639
+ "observedAttributes": [],
2286
2640
  "inputs": {
2287
2641
  "threshold": "threshold"
2288
2642
  },
@@ -2301,6 +2655,12 @@ var BUILTIN_TAGS = {
2301
2655
  },
2302
2656
  "wcs-intersect": {
2303
2657
  "package": "intersection",
2658
+ "observedAttributes": [
2659
+ "target",
2660
+ "root",
2661
+ "root-margin",
2662
+ "threshold"
2663
+ ],
2304
2664
  "inputs": {
2305
2665
  "target": "target",
2306
2666
  "root": "root",
@@ -2328,6 +2688,7 @@ var BUILTIN_TAGS = {
2328
2688
  },
2329
2689
  "wcs-magnetometer": {
2330
2690
  "package": "magnetometer",
2691
+ "observedAttributes": [],
2331
2692
  "inputs": {
2332
2693
  "frequency": null
2333
2694
  },
@@ -2343,8 +2704,46 @@ var BUILTIN_TAGS = {
2343
2704
  "stop"
2344
2705
  ]
2345
2706
  },
2707
+ "wcs-midi": {
2708
+ "package": "midi",
2709
+ "observedAttributes": [
2710
+ "input",
2711
+ "output",
2712
+ "channel"
2713
+ ],
2714
+ "inputs": {
2715
+ "input": "input",
2716
+ "output": "output",
2717
+ "channel": "channel",
2718
+ "sysex": "sysex",
2719
+ "auto": "auto"
2720
+ },
2721
+ "properties": [
2722
+ "message",
2723
+ "type",
2724
+ "channel",
2725
+ "note",
2726
+ "velocity",
2727
+ "control",
2728
+ "value",
2729
+ "devices",
2730
+ "connected",
2731
+ "permission",
2732
+ "granted",
2733
+ "denied",
2734
+ "unsupported",
2735
+ "error",
2736
+ "errorInfo"
2737
+ ],
2738
+ "commands": [
2739
+ "request",
2740
+ "close",
2741
+ "send"
2742
+ ]
2743
+ },
2346
2744
  "wcs-network": {
2347
2745
  "package": "network",
2746
+ "observedAttributes": [],
2348
2747
  "inputs": {},
2349
2748
  "properties": [
2350
2749
  "effectiveType",
@@ -2357,6 +2756,7 @@ var BUILTIN_TAGS = {
2357
2756
  },
2358
2757
  "wcs-notify": {
2359
2758
  "package": "notification",
2759
+ "observedAttributes": [],
2360
2760
  "inputs": {
2361
2761
  "notice": null,
2362
2762
  "mode": "mode",
@@ -2392,6 +2792,7 @@ var BUILTIN_TAGS = {
2392
2792
  },
2393
2793
  "wcs-permission": {
2394
2794
  "package": "permission",
2795
+ "observedAttributes": [],
2395
2796
  "inputs": {
2396
2797
  "name": "name",
2397
2798
  "userVisibleOnly": "user-visible-only",
@@ -2408,6 +2809,9 @@ var BUILTIN_TAGS = {
2408
2809
  },
2409
2810
  "wcs-pip": {
2410
2811
  "package": "picture-in-picture",
2812
+ "observedAttributes": [
2813
+ "target"
2814
+ ],
2411
2815
  "inputs": {
2412
2816
  "target": "target"
2413
2817
  },
@@ -2423,6 +2827,9 @@ var BUILTIN_TAGS = {
2423
2827
  },
2424
2828
  "wcs-pointer-lock": {
2425
2829
  "package": "pointer-lock",
2830
+ "observedAttributes": [
2831
+ "target"
2832
+ ],
2426
2833
  "inputs": {
2427
2834
  "target": "target"
2428
2835
  },
@@ -2438,6 +2845,7 @@ var BUILTIN_TAGS = {
2438
2845
  },
2439
2846
  "wcs-raf": {
2440
2847
  "package": "raf",
2848
+ "observedAttributes": [],
2441
2849
  "inputs": {
2442
2850
  "once": "once",
2443
2851
  "repeat": "repeat",
@@ -2462,6 +2870,11 @@ var BUILTIN_TAGS = {
2462
2870
  },
2463
2871
  "wcs-resize": {
2464
2872
  "package": "resize",
2873
+ "observedAttributes": [
2874
+ "target",
2875
+ "box",
2876
+ "round"
2877
+ ],
2465
2878
  "inputs": {
2466
2879
  "target": "target",
2467
2880
  "box": "box",
@@ -2485,6 +2898,7 @@ var BUILTIN_TAGS = {
2485
2898
  },
2486
2899
  "wcs-screen-orientation": {
2487
2900
  "package": "screen-orientation",
2901
+ "observedAttributes": [],
2488
2902
  "inputs": {},
2489
2903
  "properties": [
2490
2904
  "type",
@@ -2501,6 +2915,7 @@ var BUILTIN_TAGS = {
2501
2915
  },
2502
2916
  "wcs-share": {
2503
2917
  "package": "share",
2918
+ "observedAttributes": [],
2504
2919
  "inputs": {},
2505
2920
  "properties": [
2506
2921
  "value",
@@ -2515,6 +2930,7 @@ var BUILTIN_TAGS = {
2515
2930
  },
2516
2931
  "wcs-speak": {
2517
2932
  "package": "speech",
2933
+ "observedAttributes": [],
2518
2934
  "inputs": {
2519
2935
  "say": null,
2520
2936
  "rate": "rate",
@@ -2544,6 +2960,7 @@ var BUILTIN_TAGS = {
2544
2960
  },
2545
2961
  "wcs-listen": {
2546
2962
  "package": "speech",
2963
+ "observedAttributes": [],
2547
2964
  "inputs": {
2548
2965
  "lang": "lang",
2549
2966
  "continuous": "continuous",
@@ -2571,6 +2988,9 @@ var BUILTIN_TAGS = {
2571
2988
  },
2572
2989
  "wcs-sse": {
2573
2990
  "package": "sse",
2991
+ "observedAttributes": [
2992
+ "url"
2993
+ ],
2574
2994
  "inputs": {
2575
2995
  "url": "url",
2576
2996
  "withCredentials": "with-credentials",
@@ -2595,6 +3015,10 @@ var BUILTIN_TAGS = {
2595
3015
  },
2596
3016
  "wcs-storage": {
2597
3017
  "package": "storage",
3018
+ "observedAttributes": [
3019
+ "key",
3020
+ "type"
3021
+ ],
2598
3022
  "inputs": {
2599
3023
  "key": null,
2600
3024
  "type": null,
@@ -2617,6 +3041,7 @@ var BUILTIN_TAGS = {
2617
3041
  },
2618
3042
  "wcs-tilt": {
2619
3043
  "package": "tilt",
3044
+ "observedAttributes": [],
2620
3045
  "inputs": {},
2621
3046
  "properties": [
2622
3047
  "alpha",
@@ -2635,6 +3060,9 @@ var BUILTIN_TAGS = {
2635
3060
  },
2636
3061
  "wcs-timer": {
2637
3062
  "package": "timer",
3063
+ "observedAttributes": [
3064
+ "interval"
3065
+ ],
2638
3066
  "inputs": {
2639
3067
  "interval": "interval",
2640
3068
  "once": "once",
@@ -2659,6 +3087,9 @@ var BUILTIN_TAGS = {
2659
3087
  },
2660
3088
  "wcs-upload": {
2661
3089
  "package": "upload",
3090
+ "observedAttributes": [
3091
+ "url"
3092
+ ],
2662
3093
  "inputs": {
2663
3094
  "url": null,
2664
3095
  "method": null,
@@ -2687,6 +3118,10 @@ var BUILTIN_TAGS = {
2687
3118
  },
2688
3119
  "wcs-wakelock": {
2689
3120
  "package": "wakelock",
3121
+ "observedAttributes": [
3122
+ "active",
3123
+ "type"
3124
+ ],
2690
3125
  "inputs": {
2691
3126
  "active": "active",
2692
3127
  "type": "type",
@@ -2704,6 +3139,9 @@ var BUILTIN_TAGS = {
2704
3139
  },
2705
3140
  "wcs-ws": {
2706
3141
  "package": "websocket",
3142
+ "observedAttributes": [
3143
+ "url"
3144
+ ],
2707
3145
  "inputs": {
2708
3146
  "url": "url",
2709
3147
  "protocols": "protocols",
@@ -2733,6 +3171,9 @@ var BUILTIN_TAGS = {
2733
3171
  },
2734
3172
  "wcs-worker": {
2735
3173
  "package": "worker",
3174
+ "observedAttributes": [
3175
+ "src"
3176
+ ],
2736
3177
  "inputs": {
2737
3178
  "src": "src",
2738
3179
  "type": "type",
@@ -2774,13 +3215,13 @@ var DOM_COMMON_PROPERTIES = /* @__PURE__ */ new Set([
2774
3215
  ]);
2775
3216
  var STRUCTURAL_DIRECTIVES2 = /* @__PURE__ */ new Set(["for", "if", "elseif", "else"]);
2776
3217
  var EMPTYISH_SEEDS = /* @__PURE__ */ new Set(["''", '""', "``", "null", "[]", "{}"]);
2777
- function validateIoNodes(html, bindAttribute = "data-wcs", stateTagName = "wcs-state", locale) {
3218
+ function validateIoNodes(html, bindAttribute = "data-wcs", stateTagName = "wcs-state", locale, fileReader) {
2778
3219
  const diagnostics = [];
2779
3220
  const msgs = getMessages(locale);
2780
3221
  const occurrences = findBuiltinTagOccurrences(html);
2781
3222
  if (occurrences.length === 0) return diagnostics;
2782
3223
  let statePaths = null;
2783
- const getPaths = () => statePaths ??= getStatePathsFromHtml(html, stateTagName);
3224
+ const getPaths = () => statePaths ??= getStatePathsFromHtml(html, stateTagName, fileReader);
2784
3225
  for (const occ of occurrences) {
2785
3226
  const contract = BUILTIN_TAGS[occ.tagName];
2786
3227
  if (contract.properties.length === 0 && contract.commands.length === 0 && Object.keys(contract.inputs).length === 0) continue;
@@ -3078,17 +3519,68 @@ function blankJsComments(code) {
3078
3519
  return code.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/(^|[^:])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
3079
3520
  }
3080
3521
 
3522
+ // src/service/watchDeclarationValidator.ts
3523
+ var STATE_NAME_SEPARATOR = "@";
3524
+ function validateWatchDeclarations(html, stateTagName = "wcs-state", locale) {
3525
+ const msgs = getMessages(locale);
3526
+ const out = [];
3527
+ for (const block of parseWcsScriptBlocks(html, stateTagName)) {
3528
+ const entries = analyzeWatchEntries(block.content);
3529
+ if (entries.length === 0) continue;
3530
+ const paths = analyzeStatePaths(block.content, block.stateName);
3531
+ const pathSet = new Set(paths.map((p) => p.path));
3532
+ for (const entry of entries) {
3533
+ const diagnostic = validateEntry(entry, pathSet, msgs);
3534
+ if (diagnostic === null) continue;
3535
+ out.push({
3536
+ code: diagnostic.code,
3537
+ start: block.contentStart + entry.start,
3538
+ end: block.contentStart + entry.end,
3539
+ message: diagnostic.message,
3540
+ severity: diagnostic.severity
3541
+ });
3542
+ }
3543
+ }
3544
+ return out;
3545
+ }
3546
+ function validateEntry(entry, pathSet, msgs) {
3547
+ const { key } = entry;
3548
+ const invalid = (message) => ({ code: WcsDiagnosticCode.WatchDeclarationInvalid, message, severity: "error" });
3549
+ if (key.includes(STATE_NAME_SEPARATOR)) {
3550
+ return invalid(msgs.watchKeyCrossState(key));
3551
+ }
3552
+ if (key.startsWith("$")) {
3553
+ return invalid(msgs.watchKeyReserved(key));
3554
+ }
3555
+ if (key.split(".").some((segment) => segment.length === 0)) {
3556
+ return invalid(msgs.watchKeyEmptySegment(key));
3557
+ }
3558
+ if (entry.definitelyNotFunction) {
3559
+ return invalid(msgs.watchHandlerNotFunction(key));
3560
+ }
3561
+ if (pathSet.size > 0 && !pathSet.has(key)) {
3562
+ return {
3563
+ code: WcsDiagnosticCode.WatchPathMissing,
3564
+ message: msgs.watchPathMissing(key),
3565
+ severity: "warning"
3566
+ };
3567
+ }
3568
+ return null;
3569
+ }
3570
+
3081
3571
  // src/core/validateDocument.ts
3082
3572
  function validateDocument(text, options = {}) {
3083
3573
  const bindAttribute = options.bindAttribute ?? "data-wcs";
3084
3574
  const stateTagName = options.stateTagName ?? "wcs-state";
3085
3575
  const locale = options.locale;
3576
+ const fileReader = options.fileReader;
3086
3577
  const out = [];
3087
- out.push(...validateBindings(text, bindAttribute, stateTagName, locale));
3088
- out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale));
3089
- out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale));
3578
+ out.push(...validateBindings(text, bindAttribute, stateTagName, locale, fileReader));
3579
+ out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale, fileReader));
3580
+ out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale, fileReader));
3090
3581
  out.push(...validateDocumentEnv(text, locale));
3091
3582
  out.push(...validateArrayMutations(text, stateTagName, locale));
3583
+ out.push(...validateWatchDeclarations(text, stateTagName, locale));
3092
3584
  for (const d of validateStateTypes(text, stateTagName, locale)) {
3093
3585
  out.push({ code: WcsDiagnosticCode.TypeAnnotation, start: d.start, end: d.end, message: d.message, severity: d.severity });
3094
3586
  }
@@ -3672,7 +4164,8 @@ function runValidation(inputs, options = {}) {
3672
4164
  const diagnosticsBySource = /* @__PURE__ */ new Map();
3673
4165
  for (const input of inputs) {
3674
4166
  if (input.kind === "html") {
3675
- diagnosticsBySource.set(input.source, validateDocument(input.text, options));
4167
+ const docOptions = input.fileReader !== void 0 ? { ...options, fileReader: input.fileReader } : options;
4168
+ diagnosticsBySource.set(input.source, validateDocument(input.text, docOptions));
3676
4169
  }
3677
4170
  }
3678
4171
  const manifestInputs = inputs.filter((i) => i.kind === "manifest");
@@ -3716,6 +4209,29 @@ function runValidation(inputs, options = {}) {
3716
4209
  function classify(path) {
3717
4210
  return path.endsWith(".manifest.json") ? "manifest" : "html";
3718
4211
  }
4212
+ function createFileReader(htmlPath, read = (p) => (0, import_node_fs.readFileSync)(p, "utf8")) {
4213
+ const base = (0, import_node_path.dirname)(htmlPath);
4214
+ const cache = /* @__PURE__ */ new Map();
4215
+ return (relativePath) => {
4216
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(relativePath) || relativePath.startsWith("/")) {
4217
+ return void 0;
4218
+ }
4219
+ if (cache.has(relativePath)) {
4220
+ return cache.get(relativePath);
4221
+ }
4222
+ let content;
4223
+ try {
4224
+ content = read((0, import_node_path.resolve)(base, relativePath));
4225
+ if (content.charCodeAt(0) === 65279) {
4226
+ content = content.slice(1);
4227
+ }
4228
+ } catch {
4229
+ content = void 0;
4230
+ }
4231
+ cache.set(relativePath, content);
4232
+ return content;
4233
+ };
4234
+ }
3719
4235
  function parseArgs(argv) {
3720
4236
  const options = {};
3721
4237
  const files = [];
@@ -3755,7 +4271,8 @@ function main(argv) {
3755
4271
  `);
3756
4272
  return 2;
3757
4273
  }
3758
- inputs.push({ source: path, text, kind: classify(path) });
4274
+ const kind = classify(path);
4275
+ inputs.push({ source: path, text, kind, fileReader: kind === "html" ? createFileReader(path) : void 0 });
3759
4276
  }
3760
4277
  const result = runValidation(inputs, { ...options, locale });
3761
4278
  for (const line of result.lines) {
@@ -3773,6 +4290,7 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
3773
4290
  }
3774
4291
  // Annotate the CommonJS export names for ESM import in node:
3775
4292
  0 && (module.exports = {
4293
+ createFileReader,
3776
4294
  main,
3777
4295
  parseArgs,
3778
4296
  resolveCliLocale
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/lint",
3
- "version": "1.26.0",
3
+ "version": "1.28.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": {