@wcstack/lint 1.28.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.
Files changed (2) hide show
  1. package/dist/cli.cjs +64 -7
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -310,6 +310,8 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
310
310
  const jsonAttr = extractAttribute(wcsMatch.tagContent, "json") ?? void 0;
311
311
  const stateAttr = extractAttribute(wcsMatch.tagContent, "state") ?? void 0;
312
312
  const srcAttr = extractAttribute(wcsMatch.tagContent, "src") ?? void 0;
313
+ const tagStart = pos;
314
+ const tagEnd = wcsMatch.end;
313
315
  pos = wcsMatch.end;
314
316
  const scriptBlocks = [];
315
317
  const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
@@ -346,7 +348,7 @@ function parseWcsStateElements(html, stateTagName = "wcs-state") {
346
348
  pos = html.indexOf(">", scriptCloseIdx) + 1;
347
349
  if (pos === 0) break;
348
350
  }
349
- elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks });
351
+ elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks, tagStart, tagEnd });
350
352
  pos = wcsEnd;
351
353
  if (wcsCloseIdx !== -1) {
352
354
  const closeEnd = html.indexOf(">", wcsCloseIdx);
@@ -515,6 +517,26 @@ function isNonFunctionLiteral(value) {
515
517
  if (/^(?:async\s+)?function\b/.test(trimmed) || scan.includes("=>")) return false;
516
518
  return /^["'`]/.test(trimmed) || /^-?\d/.test(trimmed) || /^(?:true|false|null|undefined)\b/.test(trimmed) || trimmed.startsWith("[") || trimmed.startsWith("{");
517
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
+ }
518
540
  function collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys, stateName) {
519
541
  if (prop.name === RESERVED_STREAMS_KEY && prop.kind === "data" && prop.value && isObjectLiteral(prop.value)) {
520
542
  const entries = parseTopLevelProperties(extractObjectContent(prop.value));
@@ -966,25 +988,27 @@ function isInsideForTemplate(html, offset, bindAttrName = "data-wcs") {
966
988
  return false;
967
989
  }
968
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") {
969
995
  const escaped = bindAttrName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
970
996
  const openRegex = new RegExp(
971
997
  `<template[^>]*${escaped}\\s*=\\s*["']\\s*for\\s*:\\s*([^"']+?)\\s*["']`,
972
998
  "gi"
973
999
  );
974
- let bestMatch = null;
975
- let bestPos = -1;
1000
+ const enclosing = [];
976
1001
  let match;
977
1002
  while ((match = openRegex.exec(html)) !== null) {
978
1003
  if (match.index >= offset) break;
979
1004
  const tagEnd = html.indexOf(">", match.index);
980
1005
  if (tagEnd === -1 || tagEnd >= offset) continue;
981
1006
  const depth = getForTemplateDepthAt(html, match.index, offset, bindAttrName);
982
- if (depth > 0 && match.index > bestPos) {
983
- bestMatch = match[1].trim();
984
- bestPos = match.index;
1007
+ if (depth > 0) {
1008
+ enclosing.push(match[1].trim());
985
1009
  }
986
1010
  }
987
- return bestMatch;
1011
+ return enclosing;
988
1012
  }
989
1013
  function getForTemplateDepthAt(html, openPos, offset, bindAttrName) {
990
1014
  const tagEnd = html.indexOf(">", openPos);
@@ -1028,6 +1052,7 @@ var JA_EXPECTED_LABEL = {
1028
1052
  var ja = {
1029
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`,
1030
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`,
1031
1056
  eventTokenUndeclared: (t) => `\u30A4\u30D9\u30F3\u30C8\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $eventTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
1032
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`,
1033
1058
  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 +1073,7 @@ var ja = {
1048
1073
  wcsTextInfo: (e) => `wcs-text \u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0: ${e}`,
1049
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`,
1050
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`,
1051
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`,
1052
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`,
1053
1079
  watchKeyEmptySegment: (k) => `$watch \u306E\u30AD\u30FC "${k}" \u306B\u7A7A\u306E\u30D1\u30B9\u30BB\u30B0\u30E1\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059`,
@@ -1075,6 +1101,7 @@ var EN_EXPECTED_LABEL = {
1075
1101
  var en = {
1076
1102
  spreadFilterNotAllowed: () => `Filters cannot be applied to a spread target`,
1077
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)`,
1078
1105
  eventTokenUndeclared: (t) => `Event token "${t}" is not declared in $eventTokens`,
1079
1106
  commandRhsFormat: () => `The right side of a command binding must be $command.<name> (declared in $commandTokens)`,
1080
1107
  commandTokenUndeclared: (t) => `Command token "${t}" is not declared in $commandTokens`,
@@ -1095,6 +1122,7 @@ var en = {
1095
1122
  wcsTextInfo: (e) => `wcs-text binding: ${e}`,
1096
1123
  moustacheFouc: (e) => `{{ }} outside a <template> causes FOUC (the raw template string is visible before binding). Consider the comment syntax <!--@@:${e}--> instead.`,
1097
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)`,
1098
1126
  watchKeyCrossState: (k) => `$watch key "${k}" targets another state. Cross-state watching with @ is not supported (own paths only)`,
1099
1127
  watchKeyReserved: (k) => `$watch key "${k}" must not start with "$" (reserved namespace)`,
1100
1128
  watchKeyEmptySegment: (k) => `$watch key "${k}" has an empty path segment`,
@@ -1140,6 +1168,25 @@ function validateBindings(html, attrName, stateTagName = "wcs-state", locale, fi
1140
1168
  const filterNameSet = new Set(BUILTIN_FILTERS.map((f) => f.name));
1141
1169
  for (const attr of attrs) {
1142
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
+ }
1143
1190
  let pos = 0;
1144
1191
  for (const binding of bindings) {
1145
1192
  const bindingStart = attr.valueStart + pos;
@@ -3525,6 +3572,16 @@ function validateWatchDeclarations(html, stateTagName = "wcs-state", locale) {
3525
3572
  const msgs = getMessages(locale);
3526
3573
  const out = [];
3527
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
+ }
3528
3585
  const entries = analyzeWatchEntries(block.content);
3529
3586
  if (entries.length === 0) continue;
3530
3587
  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.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": {