@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.
- package/dist/cli.cjs +64 -7
- 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
|
-
|
|
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
|
|
983
|
-
|
|
984
|
-
bestPos = match.index;
|
|
1007
|
+
if (depth > 0) {
|
|
1008
|
+
enclosing.push(match[1].trim());
|
|
985
1009
|
}
|
|
986
1010
|
}
|
|
987
|
-
return
|
|
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.
|
|
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": {
|