@wcstack/lint 1.27.0 → 1.29.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) {
@@ -190,9 +192,24 @@ var STRUCTURAL_BINDING_TYPE_SET = /* @__PURE__ */ new Set([
190
192
  "for"
191
193
  ]);
192
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 = "$";
193
210
  var tmpIndexByIndexName = {};
194
211
  for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
195
- tmpIndexByIndexName[`$${i + 1}`] = i;
212
+ tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
196
213
  }
197
214
  Object.freeze(tmpIndexByIndexName);
198
215
 
@@ -293,6 +310,8 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
293
310
  const jsonAttr = extractAttribute(wcsMatch.tagContent, "json") ?? void 0;
294
311
  const stateAttr = extractAttribute(wcsMatch.tagContent, "state") ?? void 0;
295
312
  const srcAttr = extractAttribute(wcsMatch.tagContent, "src") ?? void 0;
313
+ const tagStart = pos;
314
+ const tagEnd = wcsMatch.end;
296
315
  pos = wcsMatch.end;
297
316
  const scriptBlocks = [];
298
317
  const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
@@ -329,7 +348,7 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
329
348
  pos = html.indexOf(">", scriptCloseIdx) + 1;
330
349
  if (pos === 0) break;
331
350
  }
332
- elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks });
351
+ elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks, tagStart, tagEnd });
333
352
  pos = wcsEnd;
334
353
  if (wcsCloseIdx !== -1) {
335
354
  const closeEnd = html.indexOf(">", wcsCloseIdx);
@@ -498,6 +517,26 @@ function isNonFunctionLiteral(value) {
498
517
  if (/^(?:async\s+)?function\b/.test(trimmed) || scan.includes("=>")) return false;
499
518
  return /^["'`]/.test(trimmed) || /^-?\d/.test(trimmed) || /^(?:true|false|null|undefined)\b/.test(trimmed) || trimmed.startsWith("[") || trimmed.startsWith("{");
500
519
  }
520
+ function findNonObjectWatch(scriptContent) {
521
+ const root = locateDefaultExportObject(scriptContent);
522
+ if (!root) return null;
523
+ const watchProp = parseTopLevelProperties(root.content).find((p) => p.name === RESERVED_WATCH_KEY);
524
+ if (!watchProp || watchProp.nameStart === void 0 || watchProp.nameEnd === void 0) {
525
+ return null;
526
+ }
527
+ const span = { start: root.start + watchProp.nameStart, end: root.start + watchProp.nameEnd };
528
+ if (watchProp.kind === "method") {
529
+ return span;
530
+ }
531
+ if (watchProp.kind !== "data" || !watchProp.value) return null;
532
+ const trimmed = watchProp.value.trim();
533
+ if (trimmed.startsWith("{")) return null;
534
+ const scan = maskCommentsAndStrings(trimmed).trim();
535
+ const isArrowFunction = /^(?:async\s+)?\([^()]*\)\s*=>/.test(scan) || /^(?:async\s+)?[$\w]+\s*=>/.test(scan);
536
+ const isWholeLiteral = /^(["'`])[^"'`]*\1$/.test(scan) || /^-?\d[\w.]*$/.test(scan) || /^(?:true|false|null)$/.test(scan) || /^(?:async\s+)?function\b[\s\S]*\}$/.test(scan);
537
+ if (!isArrowFunction && !isWholeLiteral) return null;
538
+ return span;
539
+ }
501
540
  function collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys, stateName) {
502
541
  if (prop.name === RESERVED_STREAMS_KEY && prop.kind === "data" && prop.value && isObjectLiteral(prop.value)) {
503
542
  const entries = parseTopLevelProperties(extractObjectContent(prop.value));
@@ -949,25 +988,27 @@ function isInsideForTemplate(html, offset, bindAttrName = "data-wcs") {
949
988
  return false;
950
989
  }
951
990
  function getInnermostForPath(html, offset, bindAttrName = "data-wcs") {
991
+ const chain = getEnclosingForPaths(html, offset, bindAttrName);
992
+ return chain.length === 0 ? null : chain[chain.length - 1];
993
+ }
994
+ function getEnclosingForPaths(html, offset, bindAttrName = "data-wcs") {
952
995
  const escaped = bindAttrName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
953
996
  const openRegex = new RegExp(
954
997
  `<template[^>]*${escaped}\\s*=\\s*["']\\s*for\\s*:\\s*([^"']+?)\\s*["']`,
955
998
  "gi"
956
999
  );
957
- let bestMatch = null;
958
- let bestPos = -1;
1000
+ const enclosing = [];
959
1001
  let match;
960
1002
  while ((match = openRegex.exec(html)) !== null) {
961
1003
  if (match.index >= offset) break;
962
1004
  const tagEnd = html.indexOf(">", match.index);
963
1005
  if (tagEnd === -1 || tagEnd >= offset) continue;
964
1006
  const depth = getForTemplateDepthAt(html, match.index, offset, bindAttrName);
965
- if (depth > 0 && match.index > bestPos) {
966
- bestMatch = match[1].trim();
967
- bestPos = match.index;
1007
+ if (depth > 0) {
1008
+ enclosing.push(match[1].trim());
968
1009
  }
969
1010
  }
970
- return bestMatch;
1011
+ return enclosing;
971
1012
  }
972
1013
  function getForTemplateDepthAt(html, openPos, offset, bindAttrName) {
973
1014
  const tagEnd = html.indexOf(">", openPos);
@@ -1011,6 +1052,7 @@ var JA_EXPECTED_LABEL = {
1011
1052
  var ja = {
1012
1053
  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`,
1013
1054
  spreadTargetRequired: () => `\u30B9\u30D7\u30EC\u30C3\u30C9\u306B\u306F\u30BF\u30FC\u30B2\u30C3\u30C8\u30D1\u30B9\u304C\u5FC5\u8981\u3067\u3059`,
1055
+ 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`,
1014
1056
  eventTokenUndeclared: (t) => `\u30A4\u30D9\u30F3\u30C8\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $eventTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
1015
1057
  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`,
1016
1058
  commandTokenUndeclared: (t) => `\u30B3\u30DE\u30F3\u30C9\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $commandTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
@@ -1031,6 +1073,7 @@ var ja = {
1031
1073
  wcsTextInfo: (e) => `wcs-text \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0: ${e}`,
1032
1074
  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`,
1033
1075
  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`,
1076
+ 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`,
1034
1077
  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`,
1035
1078
  watchKeyReserved: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306F "$" \u3067\u59CB\u3081\u3089\u308C\u307E\u305B\u3093\uFF08\u4E88\u7D04\u540D\u524D\u7A7A\u9593\uFF09`,
1036
1079
  watchKeyEmptySegment: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306B\u7A7A\u306E\u30D1\u30B9\u30BB\u30B0\u30E1\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059`,
@@ -1058,6 +1101,7 @@ var EN_EXPECTED_LABEL = {
1058
1101
  var en = {
1059
1102
  spreadFilterNotAllowed: () => `Filters cannot be applied to a spread target`,
1060
1103
  spreadTargetRequired: () => `Spread requires a target path`,
1104
+ structuralMustBeSingle: (d) => `'${d}' must be the only binding in this attribute (it cannot be combined with ';'; the runtime throws at load time)`,
1061
1105
  eventTokenUndeclared: (t) => `Event token "${t}" is not declared in $eventTokens`,
1062
1106
  commandRhsFormat: () => `The right side of a command binding must be $command.<name> (declared in $commandTokens)`,
1063
1107
  commandTokenUndeclared: (t) => `Command token "${t}" is not declared in $commandTokens`,
@@ -1078,6 +1122,7 @@ var en = {
1078
1122
  wcsTextInfo: (e) => `wcs-text binding: ${e}`,
1079
1123
  moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
1080
1124
  nestedAssign: (sp) => `Assigning to a nested property does not trigger a reactive update. Use this["${sp}"] instead.`,
1125
+ watchNotObject: () => `$watch must be an object mapping state paths to handler functions (the runtime throws on this shape at load time)`,
1081
1126
  watchKeyCrossState: (k) => `$watch key "${k}" targets another state. Cross-state watching with @ is not supported (own paths only)`,
1082
1127
  watchKeyReserved: (k) => `$watch key "${k}" must not start with "$" (reserved namespace)`,
1083
1128
  watchKeyEmptySegment: (k) => `$watch key "${k}" has an empty path segment`,
@@ -1104,10 +1149,10 @@ function getMessages(locale) {
1104
1149
 
1105
1150
  // src/service/bindingValidator.ts
1106
1151
  var filterMap = new Map(BUILTIN_FILTERS.map((f) => [f.name, f]));
1107
- function validateBindings(html, attrName, stateTagName = "wcs-state", locale) {
1152
+ function validateBindings(html, attrName, stateTagName = "wcs-state", locale, fileReader) {
1108
1153
  const diagnostics = [];
1109
1154
  const msgs = getMessages(locale);
1110
- const statePaths = getStatePathsFromHtml(html, stateTagName);
1155
+ const statePaths = getStatePathsFromHtml(html, stateTagName, fileReader);
1111
1156
  const pathsByState = /* @__PURE__ */ new Map();
1112
1157
  for (const p of statePaths) {
1113
1158
  const list = pathsByState.get(p.stateName) ?? [];
@@ -1123,6 +1168,25 @@ function validateBindings(html, attrName, stateTagName = "wcs-state", locale) {
1123
1168
  const filterNameSet = new Set(BUILTIN_FILTERS.map((f) => f.name));
1124
1169
  for (const attr of attrs) {
1125
1170
  const bindings = splitBindingExpressions(attr.value);
1171
+ const nonEmptyCount = bindings.filter((b) => b.trim().length > 0).length;
1172
+ if (nonEmptyCount > 1) {
1173
+ let scanPos = 0;
1174
+ for (const b of bindings) {
1175
+ const colon = b.indexOf(":");
1176
+ const prop = (colon === -1 ? b : b.slice(0, colon)).trim();
1177
+ if (STRUCTURAL_BINDING_TYPE_SET.has(prop)) {
1178
+ const leading = b.length - b.trimStart().length;
1179
+ diagnostics.push({
1180
+ code: WcsDiagnosticCode.TemplateSyntax,
1181
+ start: attr.valueStart + scanPos + leading,
1182
+ end: attr.valueStart + scanPos + b.trimEnd().length,
1183
+ message: msgs.structuralMustBeSingle(prop),
1184
+ severity: "error"
1185
+ });
1186
+ }
1187
+ scanPos += b.length + 1;
1188
+ }
1189
+ }
1126
1190
  let pos = 0;
1127
1191
  for (const binding of bindings) {
1128
1192
  const bindingStart = attr.valueStart + pos;
@@ -1898,10 +1962,10 @@ function isInsideTag(html, offset, tagName) {
1898
1962
  }
1899
1963
 
1900
1964
  // src/service/templateSyntaxValidator.ts
1901
- function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale) {
1965
+ function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale, fileReader) {
1902
1966
  const diagnostics = [];
1903
1967
  const msgs = getMessages(locale);
1904
- const allPaths = getStatePathsFromHtml(html, stateTagName);
1968
+ const allPaths = getStatePathsFromHtml(html, stateTagName, fileReader);
1905
1969
  if (allPaths.length === 0) return diagnostics;
1906
1970
  const defaultPaths = allPaths.filter((p) => p.stateName === "default");
1907
1971
  const pathSet = new Set(defaultPaths.map((p) => p.path));
@@ -2015,6 +2079,7 @@ function isValidTemplatePath(path, pathSet, scopedPaths) {
2015
2079
  var BUILTIN_TAGS = {
2016
2080
  "wcs-accelerometer": {
2017
2081
  "package": "accelerometer",
2082
+ "observedAttributes": [],
2018
2083
  "inputs": {
2019
2084
  "frequency": null
2020
2085
  },
@@ -2032,6 +2097,7 @@ var BUILTIN_TAGS = {
2032
2097
  },
2033
2098
  "wcs-ambient-light-sensor": {
2034
2099
  "package": "ambient-light-sensor",
2100
+ "observedAttributes": [],
2035
2101
  "inputs": {
2036
2102
  "frequency": null
2037
2103
  },
@@ -2045,8 +2111,240 @@ var BUILTIN_TAGS = {
2045
2111
  "stop"
2046
2112
  ]
2047
2113
  },
2114
+ "wcs-audio": {
2115
+ "package": "audio",
2116
+ "observedAttributes": [
2117
+ "volume",
2118
+ "limiter",
2119
+ "resume-on-gesture"
2120
+ ],
2121
+ "inputs": {
2122
+ "volume": "volume",
2123
+ "limiter": "limiter",
2124
+ "resumeOnGesture": "resume-on-gesture"
2125
+ },
2126
+ "properties": [
2127
+ "state",
2128
+ "running",
2129
+ "suspended",
2130
+ "unsupported",
2131
+ "voices",
2132
+ "noteOn",
2133
+ "noteOff",
2134
+ "warnings",
2135
+ "error",
2136
+ "errorInfo"
2137
+ ],
2138
+ "commands": [
2139
+ "resume",
2140
+ "suspend",
2141
+ "noteOn",
2142
+ "noteOff",
2143
+ "allNotesOff"
2144
+ ]
2145
+ },
2146
+ "wcs-voice": {
2147
+ "package": "audio",
2148
+ "observedAttributes": [
2149
+ "poly"
2150
+ ],
2151
+ "inputs": {},
2152
+ "properties": [],
2153
+ "commands": []
2154
+ },
2155
+ "wcs-osc": {
2156
+ "package": "audio",
2157
+ "observedAttributes": [
2158
+ "frequency",
2159
+ "detune",
2160
+ "type",
2161
+ "glide",
2162
+ "transpose",
2163
+ "id",
2164
+ "out",
2165
+ "param",
2166
+ "note",
2167
+ "master",
2168
+ "poly"
2169
+ ],
2170
+ "inputs": {
2171
+ "frequency": "frequency",
2172
+ "detune": "detune",
2173
+ "type": "type",
2174
+ "glide": "glide",
2175
+ "transpose": "transpose"
2176
+ },
2177
+ "properties": [],
2178
+ "commands": []
2179
+ },
2180
+ "wcs-noise": {
2181
+ "package": "audio",
2182
+ "observedAttributes": [
2183
+ "id",
2184
+ "out",
2185
+ "param",
2186
+ "note",
2187
+ "master",
2188
+ "poly"
2189
+ ],
2190
+ "inputs": {},
2191
+ "properties": [],
2192
+ "commands": []
2193
+ },
2194
+ "wcs-biquad": {
2195
+ "package": "audio",
2196
+ "observedAttributes": [
2197
+ "frequency",
2198
+ "q",
2199
+ "gain",
2200
+ "detune",
2201
+ "type",
2202
+ "id",
2203
+ "out",
2204
+ "param",
2205
+ "note",
2206
+ "master",
2207
+ "poly"
2208
+ ],
2209
+ "inputs": {
2210
+ "frequency": "frequency",
2211
+ "q": "q",
2212
+ "gain": "gain",
2213
+ "detune": "detune",
2214
+ "type": "type"
2215
+ },
2216
+ "properties": [],
2217
+ "commands": []
2218
+ },
2219
+ "wcs-gain": {
2220
+ "package": "audio",
2221
+ "observedAttributes": [
2222
+ "gain",
2223
+ "id",
2224
+ "out",
2225
+ "param",
2226
+ "note",
2227
+ "master",
2228
+ "poly"
2229
+ ],
2230
+ "inputs": {
2231
+ "gain": "gain"
2232
+ },
2233
+ "properties": [],
2234
+ "commands": []
2235
+ },
2236
+ "wcs-delay": {
2237
+ "package": "audio",
2238
+ "observedAttributes": [
2239
+ "time",
2240
+ "feedback",
2241
+ "mix",
2242
+ "id",
2243
+ "out",
2244
+ "param",
2245
+ "note",
2246
+ "master",
2247
+ "poly"
2248
+ ],
2249
+ "inputs": {
2250
+ "time": "time",
2251
+ "feedback": "feedback",
2252
+ "mix": "mix"
2253
+ },
2254
+ "properties": [],
2255
+ "commands": []
2256
+ },
2257
+ "wcs-shaper": {
2258
+ "package": "audio",
2259
+ "observedAttributes": [
2260
+ "amount",
2261
+ "id",
2262
+ "out",
2263
+ "param",
2264
+ "note",
2265
+ "master",
2266
+ "poly"
2267
+ ],
2268
+ "inputs": {
2269
+ "amount": "amount"
2270
+ },
2271
+ "properties": [],
2272
+ "commands": []
2273
+ },
2274
+ "wcs-env": {
2275
+ "package": "audio",
2276
+ "observedAttributes": [
2277
+ "attack",
2278
+ "decay",
2279
+ "sustain",
2280
+ "release",
2281
+ "depth",
2282
+ "id",
2283
+ "out",
2284
+ "param",
2285
+ "note",
2286
+ "master",
2287
+ "poly"
2288
+ ],
2289
+ "inputs": {
2290
+ "attack": "attack",
2291
+ "decay": "decay",
2292
+ "sustain": "sustain",
2293
+ "release": "release",
2294
+ "depth": "depth"
2295
+ },
2296
+ "properties": [],
2297
+ "commands": []
2298
+ },
2299
+ "wcs-lfo": {
2300
+ "package": "audio",
2301
+ "observedAttributes": [
2302
+ "rate",
2303
+ "depth",
2304
+ "type",
2305
+ "id",
2306
+ "out",
2307
+ "param",
2308
+ "note",
2309
+ "master",
2310
+ "poly"
2311
+ ],
2312
+ "inputs": {
2313
+ "rate": "rate",
2314
+ "depth": "depth",
2315
+ "type": "type"
2316
+ },
2317
+ "properties": [],
2318
+ "commands": []
2319
+ },
2320
+ "wcs-analyser": {
2321
+ "package": "audio",
2322
+ "observedAttributes": [
2323
+ "fft",
2324
+ "smoothing",
2325
+ "id",
2326
+ "out",
2327
+ "param",
2328
+ "note",
2329
+ "master",
2330
+ "poly"
2331
+ ],
2332
+ "inputs": {
2333
+ "fft": "fft",
2334
+ "smoothing": "smoothing"
2335
+ },
2336
+ "properties": [
2337
+ "frame"
2338
+ ],
2339
+ "commands": [
2340
+ "sample"
2341
+ ]
2342
+ },
2048
2343
  "wcs-broadcast": {
2049
2344
  "package": "broadcast",
2345
+ "observedAttributes": [
2346
+ "name"
2347
+ ],
2050
2348
  "inputs": {
2051
2349
  "name": "name",
2052
2350
  "manual": "manual"
@@ -2064,6 +2362,13 @@ var BUILTIN_TAGS = {
2064
2362
  },
2065
2363
  "wcs-camera": {
2066
2364
  "package": "camera",
2365
+ "observedAttributes": [
2366
+ "facing-mode",
2367
+ "device-id",
2368
+ "audio",
2369
+ "width",
2370
+ "height"
2371
+ ],
2067
2372
  "inputs": {
2068
2373
  "audio": "audio",
2069
2374
  "facingMode": "facing-mode",
@@ -2092,6 +2397,7 @@ var BUILTIN_TAGS = {
2092
2397
  },
2093
2398
  "wcs-recorder": {
2094
2399
  "package": "camera",
2400
+ "observedAttributes": [],
2095
2401
  "inputs": {
2096
2402
  "mimeType": "mime-type",
2097
2403
  "timeslice": "timeslice",
@@ -2120,6 +2426,7 @@ var BUILTIN_TAGS = {
2120
2426
  },
2121
2427
  "wcs-clipboard": {
2122
2428
  "package": "clipboard",
2429
+ "observedAttributes": [],
2123
2430
  "inputs": {
2124
2431
  "monitor": "monitor"
2125
2432
  },
@@ -2147,6 +2454,7 @@ var BUILTIN_TAGS = {
2147
2454
  },
2148
2455
  "wcs-contacts": {
2149
2456
  "package": "contacts",
2457
+ "observedAttributes": [],
2150
2458
  "inputs": {},
2151
2459
  "properties": [
2152
2460
  "value",
@@ -2161,6 +2469,7 @@ var BUILTIN_TAGS = {
2161
2469
  },
2162
2470
  "wcs-credential": {
2163
2471
  "package": "credential",
2472
+ "observedAttributes": [],
2164
2473
  "inputs": {},
2165
2474
  "properties": [
2166
2475
  "value",
@@ -2176,6 +2485,7 @@ var BUILTIN_TAGS = {
2176
2485
  },
2177
2486
  "wcs-debounce": {
2178
2487
  "package": "debounce",
2488
+ "observedAttributes": [],
2179
2489
  "inputs": {
2180
2490
  "source": null,
2181
2491
  "wait": "wait",
@@ -2196,6 +2506,7 @@ var BUILTIN_TAGS = {
2196
2506
  },
2197
2507
  "wcs-throttle": {
2198
2508
  "package": "debounce",
2509
+ "observedAttributes": [],
2199
2510
  "inputs": {
2200
2511
  "source": null,
2201
2512
  "wait": "wait",
@@ -2216,6 +2527,7 @@ var BUILTIN_TAGS = {
2216
2527
  },
2217
2528
  "wcs-defined": {
2218
2529
  "package": "defined",
2530
+ "observedAttributes": [],
2219
2531
  "inputs": {
2220
2532
  "tags": "tags",
2221
2533
  "mode": "mode",
@@ -2233,6 +2545,7 @@ var BUILTIN_TAGS = {
2233
2545
  },
2234
2546
  "wcs-eyedropper": {
2235
2547
  "package": "eyedropper",
2548
+ "observedAttributes": [],
2236
2549
  "inputs": {},
2237
2550
  "properties": [
2238
2551
  "value",
@@ -2248,6 +2561,9 @@ var BUILTIN_TAGS = {
2248
2561
  },
2249
2562
  "wcs-fetch": {
2250
2563
  "package": "fetch",
2564
+ "observedAttributes": [
2565
+ "url"
2566
+ ],
2251
2567
  "inputs": {
2252
2568
  "url": null,
2253
2569
  "method": null,
@@ -2273,24 +2589,36 @@ var BUILTIN_TAGS = {
2273
2589
  },
2274
2590
  "wcs-fetch-header": {
2275
2591
  "package": "fetch",
2592
+ "observedAttributes": [],
2276
2593
  "inputs": {},
2277
2594
  "properties": [],
2278
2595
  "commands": []
2279
2596
  },
2280
2597
  "wcs-fetch-body": {
2281
2598
  "package": "fetch",
2599
+ "observedAttributes": [],
2282
2600
  "inputs": {},
2283
2601
  "properties": [],
2284
2602
  "commands": []
2285
2603
  },
2286
2604
  "wcs-infinite-scroll": {
2287
2605
  "package": "fetch",
2606
+ "observedAttributes": [
2607
+ "target",
2608
+ "root",
2609
+ "root-margin",
2610
+ "threshold",
2611
+ "disabled"
2612
+ ],
2288
2613
  "inputs": {},
2289
2614
  "properties": [],
2290
2615
  "commands": []
2291
2616
  },
2292
2617
  "wcs-fullscreen": {
2293
2618
  "package": "fullscreen",
2619
+ "observedAttributes": [
2620
+ "target"
2621
+ ],
2294
2622
  "inputs": {
2295
2623
  "target": "target"
2296
2624
  },
@@ -2306,6 +2634,7 @@ var BUILTIN_TAGS = {
2306
2634
  },
2307
2635
  "wcs-geo": {
2308
2636
  "package": "geolocation",
2637
+ "observedAttributes": [],
2309
2638
  "inputs": {
2310
2639
  "highAccuracy": "high-accuracy",
2311
2640
  "timeout": "timeout",
@@ -2336,6 +2665,7 @@ var BUILTIN_TAGS = {
2336
2665
  },
2337
2666
  "wcs-gyroscope": {
2338
2667
  "package": "gyroscope",
2668
+ "observedAttributes": [],
2339
2669
  "inputs": {
2340
2670
  "frequency": null
2341
2671
  },
@@ -2353,6 +2683,7 @@ var BUILTIN_TAGS = {
2353
2683
  },
2354
2684
  "wcs-idle": {
2355
2685
  "package": "idle",
2686
+ "observedAttributes": [],
2356
2687
  "inputs": {
2357
2688
  "threshold": "threshold"
2358
2689
  },
@@ -2371,6 +2702,12 @@ var BUILTIN_TAGS = {
2371
2702
  },
2372
2703
  "wcs-intersect": {
2373
2704
  "package": "intersection",
2705
+ "observedAttributes": [
2706
+ "target",
2707
+ "root",
2708
+ "root-margin",
2709
+ "threshold"
2710
+ ],
2374
2711
  "inputs": {
2375
2712
  "target": "target",
2376
2713
  "root": "root",
@@ -2398,6 +2735,7 @@ var BUILTIN_TAGS = {
2398
2735
  },
2399
2736
  "wcs-magnetometer": {
2400
2737
  "package": "magnetometer",
2738
+ "observedAttributes": [],
2401
2739
  "inputs": {
2402
2740
  "frequency": null
2403
2741
  },
@@ -2413,8 +2751,46 @@ var BUILTIN_TAGS = {
2413
2751
  "stop"
2414
2752
  ]
2415
2753
  },
2754
+ "wcs-midi": {
2755
+ "package": "midi",
2756
+ "observedAttributes": [
2757
+ "input",
2758
+ "output",
2759
+ "channel"
2760
+ ],
2761
+ "inputs": {
2762
+ "input": "input",
2763
+ "output": "output",
2764
+ "channel": "channel",
2765
+ "sysex": "sysex",
2766
+ "auto": "auto"
2767
+ },
2768
+ "properties": [
2769
+ "message",
2770
+ "type",
2771
+ "channel",
2772
+ "note",
2773
+ "velocity",
2774
+ "control",
2775
+ "value",
2776
+ "devices",
2777
+ "connected",
2778
+ "permission",
2779
+ "granted",
2780
+ "denied",
2781
+ "unsupported",
2782
+ "error",
2783
+ "errorInfo"
2784
+ ],
2785
+ "commands": [
2786
+ "request",
2787
+ "close",
2788
+ "send"
2789
+ ]
2790
+ },
2416
2791
  "wcs-network": {
2417
2792
  "package": "network",
2793
+ "observedAttributes": [],
2418
2794
  "inputs": {},
2419
2795
  "properties": [
2420
2796
  "effectiveType",
@@ -2427,6 +2803,7 @@ var BUILTIN_TAGS = {
2427
2803
  },
2428
2804
  "wcs-notify": {
2429
2805
  "package": "notification",
2806
+ "observedAttributes": [],
2430
2807
  "inputs": {
2431
2808
  "notice": null,
2432
2809
  "mode": "mode",
@@ -2462,6 +2839,7 @@ var BUILTIN_TAGS = {
2462
2839
  },
2463
2840
  "wcs-permission": {
2464
2841
  "package": "permission",
2842
+ "observedAttributes": [],
2465
2843
  "inputs": {
2466
2844
  "name": "name",
2467
2845
  "userVisibleOnly": "user-visible-only",
@@ -2478,6 +2856,9 @@ var BUILTIN_TAGS = {
2478
2856
  },
2479
2857
  "wcs-pip": {
2480
2858
  "package": "picture-in-picture",
2859
+ "observedAttributes": [
2860
+ "target"
2861
+ ],
2481
2862
  "inputs": {
2482
2863
  "target": "target"
2483
2864
  },
@@ -2493,6 +2874,9 @@ var BUILTIN_TAGS = {
2493
2874
  },
2494
2875
  "wcs-pointer-lock": {
2495
2876
  "package": "pointer-lock",
2877
+ "observedAttributes": [
2878
+ "target"
2879
+ ],
2496
2880
  "inputs": {
2497
2881
  "target": "target"
2498
2882
  },
@@ -2508,6 +2892,7 @@ var BUILTIN_TAGS = {
2508
2892
  },
2509
2893
  "wcs-raf": {
2510
2894
  "package": "raf",
2895
+ "observedAttributes": [],
2511
2896
  "inputs": {
2512
2897
  "once": "once",
2513
2898
  "repeat": "repeat",
@@ -2532,6 +2917,11 @@ var BUILTIN_TAGS = {
2532
2917
  },
2533
2918
  "wcs-resize": {
2534
2919
  "package": "resize",
2920
+ "observedAttributes": [
2921
+ "target",
2922
+ "box",
2923
+ "round"
2924
+ ],
2535
2925
  "inputs": {
2536
2926
  "target": "target",
2537
2927
  "box": "box",
@@ -2555,6 +2945,7 @@ var BUILTIN_TAGS = {
2555
2945
  },
2556
2946
  "wcs-screen-orientation": {
2557
2947
  "package": "screen-orientation",
2948
+ "observedAttributes": [],
2558
2949
  "inputs": {},
2559
2950
  "properties": [
2560
2951
  "type",
@@ -2571,6 +2962,7 @@ var BUILTIN_TAGS = {
2571
2962
  },
2572
2963
  "wcs-share": {
2573
2964
  "package": "share",
2965
+ "observedAttributes": [],
2574
2966
  "inputs": {},
2575
2967
  "properties": [
2576
2968
  "value",
@@ -2585,6 +2977,7 @@ var BUILTIN_TAGS = {
2585
2977
  },
2586
2978
  "wcs-speak": {
2587
2979
  "package": "speech",
2980
+ "observedAttributes": [],
2588
2981
  "inputs": {
2589
2982
  "say": null,
2590
2983
  "rate": "rate",
@@ -2614,6 +3007,7 @@ var BUILTIN_TAGS = {
2614
3007
  },
2615
3008
  "wcs-listen": {
2616
3009
  "package": "speech",
3010
+ "observedAttributes": [],
2617
3011
  "inputs": {
2618
3012
  "lang": "lang",
2619
3013
  "continuous": "continuous",
@@ -2641,6 +3035,9 @@ var BUILTIN_TAGS = {
2641
3035
  },
2642
3036
  "wcs-sse": {
2643
3037
  "package": "sse",
3038
+ "observedAttributes": [
3039
+ "url"
3040
+ ],
2644
3041
  "inputs": {
2645
3042
  "url": "url",
2646
3043
  "withCredentials": "with-credentials",
@@ -2665,6 +3062,10 @@ var BUILTIN_TAGS = {
2665
3062
  },
2666
3063
  "wcs-storage": {
2667
3064
  "package": "storage",
3065
+ "observedAttributes": [
3066
+ "key",
3067
+ "type"
3068
+ ],
2668
3069
  "inputs": {
2669
3070
  "key": null,
2670
3071
  "type": null,
@@ -2687,6 +3088,7 @@ var BUILTIN_TAGS = {
2687
3088
  },
2688
3089
  "wcs-tilt": {
2689
3090
  "package": "tilt",
3091
+ "observedAttributes": [],
2690
3092
  "inputs": {},
2691
3093
  "properties": [
2692
3094
  "alpha",
@@ -2705,6 +3107,9 @@ var BUILTIN_TAGS = {
2705
3107
  },
2706
3108
  "wcs-timer": {
2707
3109
  "package": "timer",
3110
+ "observedAttributes": [
3111
+ "interval"
3112
+ ],
2708
3113
  "inputs": {
2709
3114
  "interval": "interval",
2710
3115
  "once": "once",
@@ -2729,6 +3134,9 @@ var BUILTIN_TAGS = {
2729
3134
  },
2730
3135
  "wcs-upload": {
2731
3136
  "package": "upload",
3137
+ "observedAttributes": [
3138
+ "url"
3139
+ ],
2732
3140
  "inputs": {
2733
3141
  "url": null,
2734
3142
  "method": null,
@@ -2757,6 +3165,10 @@ var BUILTIN_TAGS = {
2757
3165
  },
2758
3166
  "wcs-wakelock": {
2759
3167
  "package": "wakelock",
3168
+ "observedAttributes": [
3169
+ "active",
3170
+ "type"
3171
+ ],
2760
3172
  "inputs": {
2761
3173
  "active": "active",
2762
3174
  "type": "type",
@@ -2774,6 +3186,9 @@ var BUILTIN_TAGS = {
2774
3186
  },
2775
3187
  "wcs-ws": {
2776
3188
  "package": "websocket",
3189
+ "observedAttributes": [
3190
+ "url"
3191
+ ],
2777
3192
  "inputs": {
2778
3193
  "url": "url",
2779
3194
  "protocols": "protocols",
@@ -2803,6 +3218,9 @@ var BUILTIN_TAGS = {
2803
3218
  },
2804
3219
  "wcs-worker": {
2805
3220
  "package": "worker",
3221
+ "observedAttributes": [
3222
+ "src"
3223
+ ],
2806
3224
  "inputs": {
2807
3225
  "src": "src",
2808
3226
  "type": "type",
@@ -2844,13 +3262,13 @@ var DOM_COMMON_PROPERTIES = /* @__PURE__ */ new Set([
2844
3262
  ]);
2845
3263
  var STRUCTURAL_DIRECTIVES2 = /* @__PURE__ */ new Set(["for", "if", "elseif", "else"]);
2846
3264
  var EMPTYISH_SEEDS = /* @__PURE__ */ new Set(["''", '""', "``", "null", "[]", "{}"]);
2847
- function validateIoNodes(html, bindAttribute = "data-wcs", stateTagName = "wcs-state", locale) {
3265
+ function validateIoNodes(html, bindAttribute = "data-wcs", stateTagName = "wcs-state", locale, fileReader) {
2848
3266
  const diagnostics = [];
2849
3267
  const msgs = getMessages(locale);
2850
3268
  const occurrences = findBuiltinTagOccurrences(html);
2851
3269
  if (occurrences.length === 0) return diagnostics;
2852
3270
  let statePaths = null;
2853
- const getPaths = () => statePaths ??= getStatePathsFromHtml(html, stateTagName);
3271
+ const getPaths = () => statePaths ??= getStatePathsFromHtml(html, stateTagName, fileReader);
2854
3272
  for (const occ of occurrences) {
2855
3273
  const contract = BUILTIN_TAGS[occ.tagName];
2856
3274
  if (contract.properties.length === 0 && contract.commands.length === 0 && Object.keys(contract.inputs).length === 0) continue;
@@ -3154,6 +3572,16 @@ function validateWatchDeclarations(html, stateTagName = "wcs-state", locale) {
3154
3572
  const msgs = getMessages(locale);
3155
3573
  const out = [];
3156
3574
  for (const block of parseWcsScriptBlocks(html, stateTagName)) {
3575
+ const nonObject = findNonObjectWatch(block.content);
3576
+ if (nonObject !== null) {
3577
+ out.push({
3578
+ code: WcsDiagnosticCode.WatchDeclarationInvalid,
3579
+ start: block.contentStart + nonObject.start,
3580
+ end: block.contentStart + nonObject.end,
3581
+ message: msgs.watchNotObject(),
3582
+ severity: "error"
3583
+ });
3584
+ }
3157
3585
  const entries = analyzeWatchEntries(block.content);
3158
3586
  if (entries.length === 0) continue;
3159
3587
  const paths = analyzeStatePaths(block.content, block.stateName);
@@ -3202,10 +3630,11 @@ function validateDocument(text, options = {}) {
3202
3630
  const bindAttribute = options.bindAttribute ?? "data-wcs";
3203
3631
  const stateTagName = options.stateTagName ?? "wcs-state";
3204
3632
  const locale = options.locale;
3633
+ const fileReader = options.fileReader;
3205
3634
  const out = [];
3206
- out.push(...validateBindings(text, bindAttribute, stateTagName, locale));
3207
- out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale));
3208
- out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale));
3635
+ out.push(...validateBindings(text, bindAttribute, stateTagName, locale, fileReader));
3636
+ out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale, fileReader));
3637
+ out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale, fileReader));
3209
3638
  out.push(...validateDocumentEnv(text, locale));
3210
3639
  out.push(...validateArrayMutations(text, stateTagName, locale));
3211
3640
  out.push(...validateWatchDeclarations(text, stateTagName, locale));
@@ -3792,7 +4221,8 @@ function runValidation(inputs, options = {}) {
3792
4221
  const diagnosticsBySource = /* @__PURE__ */ new Map();
3793
4222
  for (const input of inputs) {
3794
4223
  if (input.kind === "html") {
3795
- diagnosticsBySource.set(input.source, validateDocument(input.text, options));
4224
+ const docOptions = input.fileReader !== void 0 ? { ...options, fileReader: input.fileReader } : options;
4225
+ diagnosticsBySource.set(input.source, validateDocument(input.text, docOptions));
3796
4226
  }
3797
4227
  }
3798
4228
  const manifestInputs = inputs.filter((i) => i.kind === "manifest");
@@ -3836,6 +4266,29 @@ function runValidation(inputs, options = {}) {
3836
4266
  function classify(path) {
3837
4267
  return path.endsWith(".manifest.json") ? "manifest" : "html";
3838
4268
  }
4269
+ function createFileReader(htmlPath, read = (p) => (0, import_node_fs.readFileSync)(p, "utf8")) {
4270
+ const base = (0, import_node_path.dirname)(htmlPath);
4271
+ const cache = /* @__PURE__ */ new Map();
4272
+ return (relativePath) => {
4273
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(relativePath) || relativePath.startsWith("/")) {
4274
+ return void 0;
4275
+ }
4276
+ if (cache.has(relativePath)) {
4277
+ return cache.get(relativePath);
4278
+ }
4279
+ let content;
4280
+ try {
4281
+ content = read((0, import_node_path.resolve)(base, relativePath));
4282
+ if (content.charCodeAt(0) === 65279) {
4283
+ content = content.slice(1);
4284
+ }
4285
+ } catch {
4286
+ content = void 0;
4287
+ }
4288
+ cache.set(relativePath, content);
4289
+ return content;
4290
+ };
4291
+ }
3839
4292
  function parseArgs(argv) {
3840
4293
  const options = {};
3841
4294
  const files = [];
@@ -3875,7 +4328,8 @@ function main(argv) {
3875
4328
  `);
3876
4329
  return 2;
3877
4330
  }
3878
- inputs.push({ source: path, text, kind: classify(path) });
4331
+ const kind = classify(path);
4332
+ inputs.push({ source: path, text, kind, fileReader: kind === "html" ? createFileReader(path) : void 0 });
3879
4333
  }
3880
4334
  const result = runValidation(inputs, { ...options, locale });
3881
4335
  for (const line of result.lines) {
@@ -3893,6 +4347,7 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
3893
4347
  }
3894
4348
  // Annotate the CommonJS export names for ESM import in node:
3895
4349
  0 && (module.exports = {
4350
+ createFileReader,
3896
4351
  main,
3897
4352
  parseArgs,
3898
4353
  resolveCliLocale
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/lint",
3
- "version": "1.27.0",
3
+ "version": "1.29.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": {