@wcstack/typescript 4.7.0 → 4.9.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/index.esm.js CHANGED
@@ -641,7 +641,7 @@ function loadSchemaCore() {
641
641
  return cached;
642
642
  }
643
643
 
644
- var version = "4.7.0";
644
+ var version = "4.9.0";
645
645
  var pkg = {
646
646
  version: version};
647
647
 
@@ -115,6 +115,9 @@ var WcsDiagnosticCode = {
115
115
  // (state README「依存追跡の境界」規則 1)。ランタイムは素のアクセスと区別できないので
116
116
  // 静的にしか検出できない。対象はオブジェクトリテラル初期値のルートだけ(配列は `items`
117
117
  // の依存で足りる — 行の置換は `items` 自体を書き換え、in-place 変異は array-mutation が止める)。
118
+ // 読んだパスをページの束縛(組み込みタグの spread・書き戻す束縛・要素の出力)が書くなら error(その書き込みは
119
+ // getter に届かないと断定できる — 継ぎ目アプリ family-ledger)、スクリプトの書き込みだけなら warning(ルートも
120
+ // 置き換えるなら壊れない)。時刻・`location` を読む getter も同じ code(warning)。
118
121
  GetterUntrackedRead: "wcs/getter-untracked-read",
119
122
  OnHandlerThis: "wcs/on-handler-this",
120
123
  // --- <wcs-state> script: $watch declaration ---
@@ -1213,7 +1216,7 @@ var SENTENCES = {
1213
1216
  [
1214
1217
  29
1215
1218
  /* FilterValueNotDate */
1216
- ]: (fn, v) => `filter ${fn} requires a date value (a Date), not ${was(v)}${typeof v === "string" || typeof v === "number" ? " \u2014 make the Date where it is read: a getter returning new Date(\u2026)" : ""}`,
1219
+ ]: (fn, v) => `filter ${fn} requires a date value (a Date), not ${was(v)}${typeof v === "string" || typeof v === "number" ? " \u2014 make the Date where it is read: a getter returning new Date(\u2026)" : ""}${typeof v === "string" && /^\d{4}-\d\d-\d\d$/.test(v) ? `. A date-only string is a day, not a moment: new Date(${JSON.stringify(v)}) is midnight UTC, the day before west of Greenwich \u2014 new Date(y, m - 1, d) is that day where the page is` : ""}`,
1217
1220
  [
1218
1221
  52
1219
1222
  /* FilterStyleInvalid */
@@ -3025,7 +3028,7 @@ function tokenize(text3) {
3025
3028
  i += 2;
3026
3029
  continue;
3027
3030
  }
3028
- if ("{}[]<>()|,;:?!*.=".includes(ch)) {
3031
+ if ("{}[]<>()|&,;:?!*.=".includes(ch)) {
3029
3032
  tokens.push({ t: "punct", v: ch });
3030
3033
  i++;
3031
3034
  continue;
@@ -3074,6 +3077,7 @@ function parseJsDocType(text3) {
3074
3077
  const member = parsePostfix();
3075
3078
  if (member === null) return null;
3076
3079
  members.push(member);
3080
+ if (expect("&")) continue;
3077
3081
  if (!expect("|")) break;
3078
3082
  }
3079
3083
  if (isPunct("=>")) {
@@ -3237,12 +3241,21 @@ function readBracedType(comment, tag) {
3237
3241
  return null;
3238
3242
  }
3239
3243
  function readJsDocTypeBefore(content, propIndex) {
3244
+ const comment = jsDocCommentBefore(content, propIndex);
3245
+ return comment === void 0 ? void 0 : readBracedType(comment, "type")?.type.trim();
3246
+ }
3247
+ function readJsDocReturnsBefore(content, propIndex) {
3248
+ const comment = jsDocCommentBefore(content, propIndex);
3249
+ if (comment === void 0) return void 0;
3250
+ return (readBracedType(comment, "returns") ?? readBracedType(comment, "return"))?.type.trim();
3251
+ }
3252
+ function jsDocCommentBefore(content, propIndex) {
3240
3253
  let end = propIndex;
3241
3254
  while (end > 0 && /\s/.test(content[end - 1])) end--;
3242
3255
  if (!content.startsWith("*/", end - 2)) return void 0;
3243
3256
  const start = content.lastIndexOf("/*", end - 2);
3244
3257
  if (start === -1 || content[start + 2] !== "*") return void 0;
3245
- return readBracedType(content.slice(start, end), "type")?.type.trim();
3258
+ return content.slice(start, end);
3246
3259
  }
3247
3260
  function collectJsDocTypedefs(script, scan) {
3248
3261
  const typedefs = /* @__PURE__ */ new Map();
@@ -3301,10 +3314,10 @@ var RESERVED_WATCH_KEY = "$watch";
3301
3314
  var RESERVED_SCAN_KEY = "$scan";
3302
3315
  var RESERVED_RECURSION_KEY = RECURSION_KEY;
3303
3316
  function analyzeStatePaths(scriptContent) {
3304
- const objectContent = extractDefaultExportObject(scriptContent);
3305
- if (!objectContent) return [];
3317
+ const root = locateDefaultExportObject(scriptContent);
3318
+ if (!root || !root.content) return [];
3306
3319
  const paths = [];
3307
- const topLevelProps = parseTopLevelProperties(objectContent);
3320
+ const topLevelProps = parseTopLevelProperties(root.content);
3308
3321
  const pendingStreamValues = [];
3309
3322
  const pendingListKeys = [];
3310
3323
  const recursionSpec = specFromRecursionValue(topLevelProps.find((p) => p.name === RESERVED_RECURSION_KEY));
@@ -3329,6 +3342,7 @@ function analyzeStatePaths(scriptContent) {
3329
3342
  const settable = !recursive && effective !== "data" && effective.set;
3330
3343
  paths.push({ path: prop.name, kind: recursive ? "recursive" : "computed", ...settable ? { settable } : {} });
3331
3344
  }
3345
+ if (prop.declaredType !== void 0) pushDeclaredShapePaths(prop.name, prop.declaredType, paths, 0, typedefs);
3332
3346
  continue;
3333
3347
  }
3334
3348
  pushDataPropertyPaths(prop, paths, typedefs);
@@ -3350,7 +3364,7 @@ function analyzeStatePaths(scriptContent) {
3350
3364
  }
3351
3365
  }
3352
3366
  collectRowShapesFromAssignments(scriptContent, paths);
3353
- collectGetterResultShapes(topLevelProps, effectiveDescriptors, paths);
3367
+ collectGetterResultShapes(scriptContent, root.start, topLevelProps, effectiveDescriptors, paths);
3354
3368
  return paths;
3355
3369
  }
3356
3370
  function specFromRecursionValue(prop) {
@@ -3762,23 +3776,45 @@ var ROW_PRESERVING_METHODS = /* @__PURE__ */ new Set([
3762
3776
  ]);
3763
3777
  var ROW_PICKING_METHODS = /* @__PURE__ */ new Set(["find", "findLast", "at"]);
3764
3778
  var MAX_GETTER_SHAPE_PASSES = 4;
3765
- function collectGetterResultShapes(props, effective, paths) {
3779
+ function collectGetterResultShapes(script, rootStart, props, effective, paths) {
3766
3780
  const getters = /* @__PURE__ */ new Map();
3767
3781
  for (const prop of props) {
3768
3782
  if (prop.kind !== "getter" || prop.accessor !== "get" || prop.value === void 0) continue;
3769
3783
  if (prop.name.startsWith("$") || hasRecursionWildcard(prop.name) || effective.get(prop.name) === "data") continue;
3770
3784
  getters.set(prop.name, prop);
3771
3785
  }
3786
+ const assignments = collectAssignments(script, rootStart, props);
3772
3787
  for (let pass = 0; pass < MAX_GETTER_SHAPE_PASSES; pass++) {
3773
3788
  const before = paths.length;
3774
3789
  for (const [name, prop] of getters) {
3775
3790
  const shape = getterResultShape(prop.value, paths);
3776
3791
  if (shape !== null) applyResultShape(name, shape, paths);
3777
3792
  }
3793
+ const aliasesByBody = /* @__PURE__ */ new Map();
3794
+ for (const assignment of assignments) {
3795
+ let aliases = /* @__PURE__ */ new Map();
3796
+ if (assignment.body !== null) {
3797
+ aliases = aliasesByBody.get(assignment.body) ?? collectAliases(assignment.body, paths);
3798
+ aliasesByBody.set(assignment.body, aliases);
3799
+ }
3800
+ const shape = expressionShape(assignment.expr, paths, aliases);
3801
+ if (shape !== null) applyAssignedShape(assignment.target, shape, paths);
3802
+ }
3778
3803
  if (paths.length === before) break;
3779
3804
  }
3780
3805
  }
3781
3806
  function getterResultShape(body, paths) {
3807
+ const scan = maskCommentsAndStrings(body);
3808
+ const aliases = collectAliases(body, paths);
3809
+ const shapes = [];
3810
+ for (const m of scan.matchAll(/\breturn\b/g)) {
3811
+ const start = m.index + m[0].length;
3812
+ const shape = expressionShape(readExpression(body, scan, start), paths, aliases);
3813
+ if (shape !== null) shapes.push(shape);
3814
+ }
3815
+ return shapes.length === 0 ? null : mergeShapes(shapes);
3816
+ }
3817
+ function collectAliases(body, paths) {
3782
3818
  const scan = maskCommentsAndStrings(body);
3783
3819
  const aliases = /* @__PURE__ */ new Map();
3784
3820
  for (const m of scan.matchAll(/\b(?:const|let|var)\s+([$\w]+)\s*=(?![=>])/g)) {
@@ -3788,13 +3824,36 @@ function getterResultShape(body, paths) {
3788
3824
  const shape = expressionShape(readExpression(body, scan, start), paths, aliases);
3789
3825
  if (shape !== null) aliases.set(name, shape);
3790
3826
  }
3791
- const shapes = [];
3792
- for (const m of scan.matchAll(/\breturn\b/g)) {
3793
- const start = m.index + m[0].length;
3794
- const shape = expressionShape(readExpression(body, scan, start), paths, aliases);
3795
- if (shape !== null) shapes.push(shape);
3827
+ return aliases;
3828
+ }
3829
+ var PLAIN_ASSIGN = new RegExp(String.raw`\bthis\s*(?:\.\s*([$\w]+)|\[\s*["']([^"']+)["']\s*\])\s*=(?![=>])`, "gd");
3830
+ function collectAssignments(script, rootStart, props) {
3831
+ const scan = maskCommentsAndStrings(script);
3832
+ const bodies = props.filter((p) => (p.kind === "getter" || p.kind === "method") && p.value !== void 0 && p.valueStart !== void 0).map((p) => ({ start: rootStart + p.valueStart, body: p.value }));
3833
+ const out = [];
3834
+ PLAIN_ASSIGN.lastIndex = 0;
3835
+ let match;
3836
+ while ((match = PLAIN_ASSIGN.exec(scan)) !== null) {
3837
+ if (/\.\s*$/.test(scan.slice(Math.max(0, match.index - 8), match.index))) continue;
3838
+ const span = match.indices[1] ?? match.indices[2];
3839
+ const target = script.slice(span[0], span[1]);
3840
+ if (target.startsWith("$") || target.includes("*")) continue;
3841
+ const expr = readExpression(script, scan, match.index + match[0].length);
3842
+ const owner = bodies.find((b) => b.start <= match.index && match.index < b.start + b.body.length);
3843
+ out.push({ target, expr, body: owner?.body ?? null });
3844
+ }
3845
+ return out;
3846
+ }
3847
+ function applyAssignedShape(target, shape, paths) {
3848
+ const declared = paths.find((p) => p.path === target);
3849
+ if (declared === void 0 || declared.kind !== "data") return;
3850
+ if (hasPath(paths, `${target}.*`)) {
3851
+ if (shape.isList) applyResultShape(target, shape, paths);
3852
+ return;
3853
+ }
3854
+ if (!shape.isList && declared.rawInitial !== void 0 && isObjectLiteral(declared.rawInitial)) {
3855
+ applyResultShape(target, shape, paths);
3796
3856
  }
3797
- return shapes.length === 0 ? null : mergeShapes(shapes);
3798
3857
  }
3799
3858
  function readExpression(text3, scan, start) {
3800
3859
  let depth = 0;
@@ -3849,9 +3908,7 @@ function expressionShape(text3, paths, aliases) {
3849
3908
  }
3850
3909
  } else if (scan.startsWith("(")) {
3851
3910
  const inner = extractDelimitedContent(expr, scan, 0, "(", ")");
3852
- const base = expressionShape(inner, paths, aliases);
3853
- if (base === null) return null;
3854
- shape = base;
3911
+ shape = expressionShape(inner, paths, aliases) ?? OPAQUE;
3855
3912
  rest = inner.length + 2;
3856
3913
  } else {
3857
3914
  const base = readBase(expr, scan, aliases, paths);
@@ -4200,13 +4257,15 @@ function locateDefaultExportObject(script) {
4200
4257
  const braceIndex = scan.indexOf(match[1], match.index);
4201
4258
  return { content: extractBracedContent(script, scan, braceIndex), start: braceIndex + 1 };
4202
4259
  }
4203
- function extractDefaultExportObject(script) {
4204
- return locateDefaultExportObject(script)?.content ?? null;
4205
- }
4206
4260
  function parseTopLevelProperties(objectContent, allowEmptyDataKeys = false) {
4207
4261
  const props = [];
4208
4262
  const scan = maskCommentsAndStrings(objectContent);
4209
- const regex = allowEmptyDataKeys ? /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:"([^"]*)"|'([^']*)'|([$\w]+))\s*:\s*)/gd : /(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:async\s+)?(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*\([^)]*\)\s*\{)|(?:(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*:\s*)/gd;
4263
+ const params = String.raw`\((?:[^()]|\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\))*\)`;
4264
+ const dataKey = allowEmptyDataKeys ? String.raw`(?:"([^"]*)"|'([^']*)'|([$\w]+))` : String.raw`(?:"([^"]+)"|'([^']+)'|([$\w]+))`;
4265
+ const regex = new RegExp(
4266
+ String.raw`(?:(?:get|set)\s+(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*${params}\s*\{)|(?:(?:async\s+)?(?:"([^"]+)"|'([^']+)'|([$\w]+))\s*${params}\s*\{)|(?:${dataKey}\s*:\s*)`,
4267
+ "gd"
4268
+ );
4210
4269
  let match;
4211
4270
  while ((match = regex.exec(scan)) !== null) {
4212
4271
  const indices = match.indices;
@@ -4227,6 +4286,8 @@ function parseTopLevelProperties(objectContent, allowEmptyDataKeys = false) {
4227
4286
  if (accessorName) {
4228
4287
  const { body, bodyStart } = skipBody();
4229
4288
  const accessor = match[0].startsWith("set") ? "set" : "get";
4289
+ const returnsText = accessor === "get" ? readJsDocReturnsBefore(objectContent, match.index) : void 0;
4290
+ const declaredType = returnsText === void 0 ? null : parseJsDocType(returnsText);
4230
4291
  props.push({
4231
4292
  name: accessorName,
4232
4293
  kind: "getter",
@@ -4234,7 +4295,8 @@ function parseTopLevelProperties(objectContent, allowEmptyDataKeys = false) {
4234
4295
  value: body,
4235
4296
  valueStart: bodyStart,
4236
4297
  nameStart: nameSpan[0],
4237
- nameEnd: nameSpan[1]
4298
+ nameEnd: nameSpan[1],
4299
+ ...declaredType !== null ? { declaredType } : {}
4238
4300
  });
4239
4301
  continue;
4240
4302
  }
@@ -11389,6 +11451,7 @@ var ja = {
11389
11451
  pathReadonlyTwoWay: (p, prop) => `"${p}" \u306F setter \u306E\u7121\u3044 getter \u3067\u3059\u304C\u3001${prop}: \u306F\u8AAD\u8005\u306E\u5165\u529B\u3092\u66F8\u304D\u623B\u3059\u306E\u3067\u3001\u5165\u529B\u306E\u305F\u3073\u306B\u66F8\u304D\u8FBC\u307F\u304C\u5931\u6557\u3057\u307E\u3059\uFF08"${p}" is a getter without a setter\uFF09\u3002setter \u3092\u8DB3\u3059\uFF08set "${p}"(v) { \u2026 }\uFF09\u304B\u3001#ro \u3067\u4E00\u65B9\u5411\u306B\u3057\u3066\u304F\u3060\u3055\u3044\uFF08${prop}#ro: ${p}\uFF09`,
11390
11452
  submitWithoutPrevent: (h) => `onsubmit \u306B #prevent \u304C\u7121\u304F\u3001"${h}" \u3082 preventDefault \u3092\u547C\u3073\u307E\u305B\u3093\u3002\u30D6\u30E9\u30A6\u30B6\u306F\u30D5\u30A9\u30FC\u30E0\u3082\u9001\u308B\u306E\u3067\u3001\u30DA\u30FC\u30B8\u304C\u8AAD\u307F\u76F4\u3055\u308C\u308B\u304B\u3001router \u304C\u3042\u308C\u3070 GET \u304C\u6B04\u306E\u5024\uFF08\u30D1\u30B9\u30EF\u30FC\u30C9\u3082\uFF09\u3092 URL \u3068\u5C65\u6B74\u306B\u8F09\u305B\u307E\u3059\u3002onsubmit#prevent: ${h} \u3068\u66F8\u304F\u304B\u3001\u30CF\u30F3\u30C9\u30E9\u3067 event.preventDefault() \u3092\u547C\u3093\u3067\u304F\u3060\u3055\u3044`,
11391
11453
  getterUntrackedRead: (root, sp) => `\u3053\u3053\u3067\u8FFD\u8DE1\u3055\u308C\u308B\u306E\u306F "${root}" \u3060\u3051\u3067\u3059\u3002"${sp}" \u304C\u5909\u308F\u3063\u3066\u3082\u3053\u306E getter \u306F\u518D\u8A55\u4FA1\u3055\u308C\u307E\u305B\u3093\uFF08\u30D1\u30B9\u8AAD\u307F\u53D6\u308A\u306E\u5148\u306E\u7D20\u306E\u30D7\u30ED\u30D1\u30C6\u30A3\u30A2\u30AF\u30BB\u30B9\u306F\u8FFD\u8DE1\u3055\u308C\u306A\u3044\uFF09\u3002this["${sp}"] \u3067\u8AAD\u3093\u3067\u304F\u3060\u3055\u3044`,
11454
+ getterUntrackedReadBound: (root, sp, binding, tag, written) => `\u3053\u3053\u3067\u8FFD\u8DE1\u3055\u308C\u308B\u306E\u306F "${root}" \u3060\u3051\u3067\u3059\u304C\u3001<${tag}> \u306E\u675F\u7E1B "${binding}" \u306F "${written}" \u3092\u30D1\u30B9\u3067\u66F8\u304D\u307E\u3059\u3002\u305D\u306E\u66F8\u304D\u8FBC\u307F\u306F\u3053\u306E getter \u306B\u5C4A\u304B\u305A\u3001getter \u306F\u524D\u306B\u8A08\u7B97\u3057\u305F\u5024\u3092\u8FD4\u3057\u7D9A\u3051\u307E\u3059\uFF08\u30D1\u30B9\u8AAD\u307F\u53D6\u308A\u306E\u5148\u306E\u7D20\u306E\u30D7\u30ED\u30D1\u30C6\u30A3\u30A2\u30AF\u30BB\u30B9\u306F\u8FFD\u8DE1\u3055\u308C\u306A\u3044\uFF09\u3002this["${sp}"] \u3067\u8AAD\u3093\u3067\u304F\u3060\u3055\u3044`,
11392
11455
  handlerFilterNotAllowed: (prop) => `\u30A4\u30D9\u30F3\u30C8\u30CF\u30F3\u30C9\u30E9 "${prop}" \u306B\u30D5\u30A3\u30EB\u30BF\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`,
11393
11456
  typeExpectation: (label, expected, resultType) => `"${label}" \u306B\u306F${JA_EXPECTED_LABEL[expected]}\u304C\u5FC5\u8981\u3067\u3059\uFF08\u73FE\u5728\u306E\u578B: ${resultType}\uFF09`,
11394
11457
  filterUnknown: (n) => `\u30D5\u30A3\u30EB\u30BF "${n}" \u306F\u7D44\u307F\u8FBC\u307F\u30D5\u30A3\u30EB\u30BF\u306B\u5B58\u5728\u3057\u307E\u305B\u3093`,
@@ -11599,6 +11662,7 @@ var en = {
11599
11662
  pathReadonlyTwoWay: (p, prop) => `"${p}" is a getter without a setter, and ${prop}: writes back what the reader enters, so every edit fails ("${p}" is a getter without a setter). Add a setter (set "${p}"(v) { \u2026 }), or bind it one way with #ro (${prop}#ro: ${p})`,
11600
11663
  submitWithoutPrevent: (h) => `onsubmit has no #prevent and "${h}" does not call preventDefault, so the browser submits the form too: the page reloads, or with the router a GET puts the fields (a password too) in the URL and the history. Write onsubmit#prevent: ${h}, or call event.preventDefault() in the handler`,
11601
11664
  getterUntrackedRead: (root, sp) => `Only "${root}" is tracked here: the getter is not re-evaluated when "${sp}" changes (plain property access after a path read is not tracked). Read this["${sp}"] instead`,
11665
+ getterUntrackedReadBound: (root, sp, binding, tag, written) => `Only "${root}" is tracked here, but the binding "${binding}" on <${tag}> writes "${written}" by path: that write never reaches this getter, which keeps returning the value it last computed (plain property access after a path read is not tracked). Read this["${sp}"] instead`,
11602
11666
  handlerFilterNotAllowed: (prop) => `Filters cannot be applied to event handler "${prop}"`,
11603
11667
  typeExpectation: (label, expected, resultType) => `"${label}" requires ${EN_EXPECTED_LABEL[expected]} (current type: ${resultType})`,
11604
11668
  filterUnknown: (n) => `Filter "${n}" is not a built-in filter`,
@@ -11916,7 +11980,7 @@ function forEachStateMethod(elements, html, fileReader, spelling, fn) {
11916
11980
  for (const callable of analyzeCallableBodies(script)) {
11917
11981
  if (callable.kind !== "method" || spelling !== null && !callable.body.includes(spelling)) continue;
11918
11982
  const head = script.slice(callable.end, callable.bodyStart - 1);
11919
- const params = /\(([^)]*)\)\s*$/.exec(head);
11983
+ const params = /\(([\s\S]*)\)\s*$/.exec(head);
11920
11984
  if (params === null) continue;
11921
11985
  fn(volume === null ? callable.name : `${volume}.${callable.name}`, params[1], callable.body);
11922
11986
  }
@@ -12336,7 +12400,8 @@ function openTagBefore(html, valueStart) {
12336
12400
  return lt2 === -1 ? "" : html.slice(lt2, valueStart);
12337
12401
  }
12338
12402
  function writesBack(attr, prop, written) {
12339
- if (written.split("#").slice(1).some((modifier) => modifier.trim() === "ro")) return false;
12403
+ const modifiers = written.split("#").slice(1).flatMap((part) => part.split(","));
12404
+ if (modifiers.some((modifier) => modifier.trim() === "ro")) return false;
12340
12405
  if (prop === "radio" || prop === "checkbox") return true;
12341
12406
  const tag = attr.tagName;
12342
12407
  if (tag === void 0 || tag.includes("-")) return false;
@@ -14279,6 +14344,10 @@ var BUILTIN_TAGS = {
14279
14344
  "withCredentials": "with-credentials",
14280
14345
  "events": "events",
14281
14346
  "raw": "raw",
14347
+ "autoReconnect": "auto-reconnect",
14348
+ "reconnectInterval": "reconnect-interval",
14349
+ "maxReconnects": "max-reconnects",
14350
+ "staleAfter": "stale-after",
14282
14351
  "manual": "manual",
14283
14352
  "trigger": null
14284
14353
  },
@@ -14405,6 +14474,14 @@ var BUILTIN_TAGS = {
14405
14474
  "abort"
14406
14475
  ]
14407
14476
  },
14477
+ "wcs-upload-header": {
14478
+ "package": "upload",
14479
+ "hasWcBindable": false,
14480
+ "observedAttributes": [],
14481
+ "inputs": {},
14482
+ "properties": [],
14483
+ "commands": []
14484
+ },
14408
14485
  "wcs-view-transition": {
14409
14486
  "package": "view-transition",
14410
14487
  "hasWcBindable": true,
@@ -14469,6 +14546,7 @@ var BUILTIN_TAGS = {
14469
14546
  "reconnectInterval": "reconnect-interval",
14470
14547
  "maxReconnects": "max-reconnects",
14471
14548
  "binaryType": "binary-type",
14549
+ "staleAfter": "stale-after",
14472
14550
  "manual": "manual",
14473
14551
  "trigger": null,
14474
14552
  "send": null
@@ -16692,7 +16770,7 @@ function validateGetterCycles(script, scriptStart, locale2) {
16692
16770
  }
16693
16771
  return out;
16694
16772
  }
16695
- function validateGetterUntrackedReads(script, scriptStart, nestedWriteRoots, locale2) {
16773
+ function validateGetterUntrackedReads(script, scriptStart, mountPath, nestedWriteRoots, bindingWrites, locale2) {
16696
16774
  const getters = analyzeCallableBodies(script).filter((entry) => entry.kind === "getter" && entry.accessor === "get");
16697
16775
  if (getters.length === 0) return [];
16698
16776
  const objectRoots = /* @__PURE__ */ new Set();
@@ -16710,18 +16788,66 @@ function validateGetterUntrackedReads(script, scriptStart, nestedWriteRoots, loc
16710
16788
  if (read.written) continue;
16711
16789
  const segments = read.callee ? read.chain.slice(0, -1) : read.chain;
16712
16790
  if (segments.length < 2 || !objectRoots.has(segments[0])) continue;
16713
- if (!nestedWriteRoots().has(segments[0])) continue;
16791
+ const prefix = mountPath === null ? [] : mountPath.split(".");
16792
+ const write = boundWriteOf([...prefix, ...segments], prefix.length + read.path.split(".").length, bindingWrites());
16793
+ if (write === void 0 && !nestedWriteRoots().has([...prefix, segments[0]].join("."))) continue;
16794
+ const sp = segments.join(".");
16714
16795
  out.push({
16715
16796
  code: WcsDiagnosticCode.GetterUntrackedRead,
16716
16797
  start: scriptStart + getter.bodyStart + read.start,
16717
16798
  end: scriptStart + getter.bodyStart + read.end,
16718
- message: msgs.getterUntrackedRead(segments[0], segments.join(".")),
16719
- severity: "warning"
16799
+ message: write === void 0 ? msgs.getterUntrackedRead(segments[0], sp) : msgs.getterUntrackedReadBound(segments[0], sp, write.binding, write.tag, write.path),
16800
+ severity: write === void 0 ? "warning" : "error"
16720
16801
  });
16721
16802
  }
16722
16803
  }
16723
16804
  return out;
16724
16805
  }
16806
+ function boundWriteOf(segments, tracked, writes) {
16807
+ return writes.find((write) => {
16808
+ const written = write.path.split(".");
16809
+ if (written.length <= tracked || written.length > segments.length) return false;
16810
+ return written.every((s, i) => s === segments[i] || s === "*" && /^\d+$/.test(segments[i]));
16811
+ });
16812
+ }
16813
+ function collectBindingWrites(html, bindAttrName) {
16814
+ const out = [];
16815
+ const ignored = findIgnoredRanges(html, bindAttrName);
16816
+ for (const attr of findAllBindAttributes(html, bindAttrName)) {
16817
+ if (ignored.some(([start, end]) => attr.valueStart >= start && attr.valueStart < end)) continue;
16818
+ const tag = attr.tagName ?? "";
16819
+ const contract = BUILTIN_TAGS[tag];
16820
+ const outputs = contract?.hasWcBindable === true ? contract.properties : null;
16821
+ const bindings = parseBindTextWithPositions(attr.value);
16822
+ bindings.forEach((binding, index) => {
16823
+ const parsed = binding.parsed;
16824
+ if (parsed === null || binding.pathRange === null) return;
16825
+ let path = parsed.statePathName;
16826
+ if (path.startsWith(".")) {
16827
+ const at2 = attr.valueStart + binding.pathRange.start;
16828
+ const list = getRowShorthandForPath(html, at2, bindAttrName) === null ? null : getResolvedForListPath(html, at2, bindAttrName);
16829
+ if (list === null) return;
16830
+ path = path === "." ? `${list}.*` : `${list}.*.${path.slice(1)}`;
16831
+ }
16832
+ const write = (written) => {
16833
+ out.push({ path: written, binding: binding.exprText, tag });
16834
+ };
16835
+ if (parsed.bindingType === "spread") {
16836
+ if (outputs === null) return;
16837
+ const later = new Set(bindings.slice(index + 1).flatMap((b) => b.parsed?.bindingType === "prop" ? [b.parsed.propName] : []));
16838
+ for (const output of outputs) if (!later.has(output)) write(`${path}.${output}`);
16839
+ return;
16840
+ }
16841
+ if (parsed.propModifiers.includes("ro")) return;
16842
+ if (parsed.bindingType === "radio" || parsed.bindingType === "checkbox") {
16843
+ write(path);
16844
+ } else if (parsed.bindingType === "prop" && (writesBack(attr, parsed.propName, parsed.propName) || outputs?.includes(parsed.propName) === true)) {
16845
+ write(path);
16846
+ }
16847
+ });
16848
+ }
16849
+ return out;
16850
+ }
16725
16851
  function validateGetterLocationReads(script, scriptStart, locale2) {
16726
16852
  if (!script.includes("location")) return [];
16727
16853
  const msgs = getMessages(locale2);
@@ -17052,12 +17178,21 @@ function validateSemantics(html, stateTagName = "wcs-state", locale2, bindAttrNa
17052
17178
  const blocks = parseLoadedScriptBlocks(html, stateTagName);
17053
17179
  let nestedWriteRoots = null;
17054
17180
  const getNestedWriteRoots = () => nestedWriteRoots ??= collectNestedWriteRoots(html, stateTagName, bindAttrName, blocks);
17181
+ let bindingWrites = null;
17182
+ const getBindingWrites = () => bindingWrites ??= collectBindingWrites(html, bindAttrName);
17055
17183
  for (const block of blocks) {
17056
17184
  out.push(...validateIndexArity(block.content, block.contentStart, locale2));
17057
17185
  out.push(...validateNameAliases(block.content, block.contentStart, locale2, block.mountPath));
17058
17186
  out.push(...validateIndexParamReads(block.content, block.contentStart, locale2));
17059
17187
  out.push(...validateGetterCycles(block.content, block.contentStart, locale2));
17060
- out.push(...validateGetterUntrackedReads(block.content, block.contentStart, getNestedWriteRoots, locale2));
17188
+ out.push(...validateGetterUntrackedReads(
17189
+ block.content,
17190
+ block.contentStart,
17191
+ block.mountPath,
17192
+ getNestedWriteRoots,
17193
+ getBindingWrites,
17194
+ locale2
17195
+ ));
17061
17196
  out.push(...validateGetterLocationReads(block.content, block.contentStart, locale2));
17062
17197
  out.push(...validateGetterClockReads(block.content, block.contentStart, locale2));
17063
17198
  out.push(...validateOnHandlerThis(block.content, block.contentStart, locale2));
@@ -643,7 +643,7 @@ function loadSchemaCore() {
643
643
  return cached;
644
644
  }
645
645
 
646
- var version = "4.7.0";
646
+ var version = "4.9.0";
647
647
  var pkg = {
648
648
  version: version};
649
649
 
package/dist/wcs-tsc.mjs CHANGED
@@ -25,7 +25,7 @@ function unsupportedTypeScriptMessage(version) {
25
25
  return `@wcstack/typescript runs on TypeScript 5 or 6, and the project has TypeScript ${version}, which has no JavaScript compiler API: npm i -D typescript@6`;
26
26
  }
27
27
 
28
- var version = "4.7.0";
28
+ var version = "4.9.0";
29
29
  var pkg = {
30
30
  version: version};
31
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/typescript",
3
- "version": "4.7.0",
3
+ "version": "4.9.0",
4
4
  "description": "TypeScript tooling for wcstack apps: wcs-schema generates the sidecar stateSchema from a typed state file, so wcs-validate checks data-wcs paths against real types in CI and every editor.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.esm.js",