@wcstack/typescript 4.0.1 → 4.0.2

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
@@ -572,7 +572,7 @@ function loadSchemaCore() {
572
572
  return cached;
573
573
  }
574
574
 
575
- var version = "4.0.1";
575
+ var version = "4.0.2";
576
576
  var pkg = {
577
577
  version: version};
578
578
 
@@ -199,6 +199,13 @@ var WcsDiagnosticCode = {
199
199
  TriggerSeededTruthy: "wcs/trigger-seeded-truthy",
200
200
  // 非 manual <wcs-storage> value バインド先の空値シード(初期書き戻しが保存値を上書き)。
201
201
  StorageSeedClobber: "wcs/storage-seed-clobber",
202
+ // `value#init=element` の <wcs-storage> バインド先の null 以外のリテラルシード
203
+ // (空のキーは null を読むので、初回の引き込みでシードが null に置き換わる)。
204
+ StorageInitSeed: "wcs/storage-init-seed",
205
+ // 組み込み I/O ノードの入力を束ねたパスの下へ、ページが書き込む(`value: cart` と
206
+ // `this["cart.0.quantity"] = …`)。束縛はそのパス自身への代入でしか当て直されないので、
207
+ // 下への書き込みは要素に届かない(storage なら保存されない)。
208
+ InputNestedWrite: "wcs/input-nested-write",
202
209
  // --- accessibility (docs/a11y-design.md §8 / D9) ---
203
210
  // `attr.aria-*` バインドの属性名が WAI-ARIA に存在しない(タイポ)。
204
211
  // setAttribute はそのまま書き、支援技術は黙って無視する。severity は warning
@@ -2822,6 +2829,292 @@ function coversSuffix(spec, familySuffix, suffix) {
2822
2829
  return false;
2823
2830
  }
2824
2831
 
2832
+ // src/service/jsdocType.ts
2833
+ var UNKNOWN = { kind: "unknown" };
2834
+ var NULL_TYPE = { kind: "name", name: "null" };
2835
+ var ARRAY_GENERICS = /* @__PURE__ */ new Set(["Array", "ReadonlyArray"]);
2836
+ var MAX_RESOLVE_DEPTH = 8;
2837
+ function tokenize(text3) {
2838
+ const tokens = [];
2839
+ let i = 0;
2840
+ while (i < text3.length) {
2841
+ const ch = text3[i];
2842
+ if (/\s/.test(ch)) {
2843
+ i++;
2844
+ continue;
2845
+ }
2846
+ if (ch === "=" && text3[i + 1] === ">") {
2847
+ tokens.push({ t: "punct", v: "=>" });
2848
+ i += 2;
2849
+ continue;
2850
+ }
2851
+ if ("{}[]<>()|,;:?!*.=".includes(ch)) {
2852
+ tokens.push({ t: "punct", v: ch });
2853
+ i++;
2854
+ continue;
2855
+ }
2856
+ if (ch === '"' || ch === "'") {
2857
+ const end = text3.indexOf(ch, i + 1);
2858
+ if (end === -1) return null;
2859
+ tokens.push({ t: "string", v: text3.slice(i + 1, end) });
2860
+ i = end + 1;
2861
+ continue;
2862
+ }
2863
+ const num = /^-?\d+(?:\.\d+)?/.exec(text3.slice(i));
2864
+ if (num) {
2865
+ tokens.push({ t: "number" });
2866
+ i += num[0].length;
2867
+ continue;
2868
+ }
2869
+ const name = /^[$A-Za-z_][$\w]*/.exec(text3.slice(i));
2870
+ if (name) {
2871
+ tokens.push({ t: "name", v: name[0] });
2872
+ i += name[0].length;
2873
+ continue;
2874
+ }
2875
+ return null;
2876
+ }
2877
+ return tokens;
2878
+ }
2879
+ function parseJsDocType(text3) {
2880
+ const tokens = tokenize(text3);
2881
+ if (tokens === null || tokens.length === 0) return null;
2882
+ let pos = 0;
2883
+ const peek = () => tokens[pos];
2884
+ const isPunct = (v) => {
2885
+ const token = tokens[pos];
2886
+ return token !== void 0 && token.t === "punct" && token.v === v;
2887
+ };
2888
+ const expect = (v) => {
2889
+ if (!isPunct(v)) return false;
2890
+ pos++;
2891
+ return true;
2892
+ };
2893
+ const parseUnion = () => {
2894
+ expect("|");
2895
+ const members = [];
2896
+ for (; ; ) {
2897
+ const member = parsePostfix();
2898
+ if (member === null) return null;
2899
+ members.push(member);
2900
+ if (!expect("|")) break;
2901
+ }
2902
+ if (isPunct("=>")) {
2903
+ pos++;
2904
+ if (parseUnion() === null) return null;
2905
+ return UNKNOWN;
2906
+ }
2907
+ return members.length === 1 ? members[0] : { kind: "union", members };
2908
+ };
2909
+ const parsePostfix = () => {
2910
+ let type2 = parsePrimary();
2911
+ if (type2 === null) return null;
2912
+ while (isPunct("[") && tokens[pos + 1]?.t === "punct" && tokens[pos + 1].v === "]") {
2913
+ pos += 2;
2914
+ type2 = { kind: "array", element: type2 };
2915
+ }
2916
+ return type2;
2917
+ };
2918
+ const parsePrimary = () => {
2919
+ const token = peek();
2920
+ if (token === void 0) return null;
2921
+ if (token.t === "string" || token.t === "number") {
2922
+ pos++;
2923
+ return UNKNOWN;
2924
+ }
2925
+ if (token.t === "punct") {
2926
+ if (token.v === "{") return parseObject();
2927
+ if (token.v === "(") {
2928
+ pos++;
2929
+ if (expect(")")) return isPunct("=>") ? UNKNOWN : null;
2930
+ const inner = parseUnion();
2931
+ if (inner === null) return null;
2932
+ if (isPunct(":") || isPunct(",")) {
2933
+ if (!skipUntilClose("(", ")")) return null;
2934
+ return UNKNOWN;
2935
+ }
2936
+ return expect(")") ? inner : null;
2937
+ }
2938
+ if (token.v === "?") {
2939
+ pos++;
2940
+ const next = peek();
2941
+ if (next === void 0 || next.t === "punct" && ["|", ",", ";", ")", ">", "]", "}", "="].includes(next.v)) return UNKNOWN;
2942
+ const inner = parsePostfix();
2943
+ return inner === null ? null : { kind: "union", members: [inner, NULL_TYPE] };
2944
+ }
2945
+ if (token.v === "!") {
2946
+ pos++;
2947
+ return parsePostfix();
2948
+ }
2949
+ if (token.v === "*") {
2950
+ pos++;
2951
+ return UNKNOWN;
2952
+ }
2953
+ return null;
2954
+ }
2955
+ pos++;
2956
+ let name = token.v;
2957
+ while (isPunct(".") && tokens[pos + 1]?.t === "name") {
2958
+ name += "." + tokens[pos + 1].v;
2959
+ pos += 2;
2960
+ }
2961
+ if ((name === "typeof" || name === "keyof") && peek()?.t === "name") {
2962
+ pos++;
2963
+ return UNKNOWN;
2964
+ }
2965
+ if (name === "function" && isPunct("(")) {
2966
+ pos++;
2967
+ if (!skipUntilClose("(", ")")) return null;
2968
+ if (expect(":")) {
2969
+ if (parsePostfix() === null) return null;
2970
+ }
2971
+ return UNKNOWN;
2972
+ }
2973
+ if (isPunct(".") && tokens[pos + 1]?.t === "punct" && tokens[pos + 1].v === "<") pos++;
2974
+ if (isPunct("<")) {
2975
+ pos++;
2976
+ const args = [];
2977
+ for (; ; ) {
2978
+ const arg = parseUnion();
2979
+ if (arg === null) return null;
2980
+ args.push(arg);
2981
+ if (!expect(",")) break;
2982
+ }
2983
+ if (!expect(">")) return null;
2984
+ return ARRAY_GENERICS.has(name) && args.length === 1 ? { kind: "array", element: args[0] } : UNKNOWN;
2985
+ }
2986
+ return { kind: "name", name };
2987
+ };
2988
+ const parseObject = () => {
2989
+ pos++;
2990
+ const fields = [];
2991
+ while (!isPunct("}")) {
2992
+ const key = peek();
2993
+ if (key === void 0) return null;
2994
+ if (key.t === "punct" && key.v === "[") {
2995
+ pos++;
2996
+ if (!skipUntilClose("[", "]")) return null;
2997
+ if (!expect(":") || parseUnion() === null) return null;
2998
+ } else {
2999
+ if (key.t !== "name" && key.t !== "string") return null;
3000
+ pos++;
3001
+ expect("?");
3002
+ if (!expect(":")) return null;
3003
+ const type2 = parseUnion();
3004
+ if (type2 === null) return null;
3005
+ fields.push({ name: key.v, type: type2 });
3006
+ }
3007
+ if (!expect(",") && !expect(";")) break;
3008
+ }
3009
+ return expect("}") ? { kind: "object", fields } : null;
3010
+ };
3011
+ const skipUntilClose = (open, close) => {
3012
+ let depth = 0;
3013
+ while (pos < tokens.length) {
3014
+ const token = tokens[pos++];
3015
+ if (token.t !== "punct") continue;
3016
+ if (token.v === open) depth++;
3017
+ else if (token.v === close) {
3018
+ if (depth === 0) return true;
3019
+ depth--;
3020
+ }
3021
+ }
3022
+ return false;
3023
+ };
3024
+ const type = parseUnion();
3025
+ if (type === null || pos !== tokens.length) return null;
3026
+ return type;
3027
+ }
3028
+ function findJsDocComments(script, scan) {
3029
+ const out = [];
3030
+ let from = 0;
3031
+ for (; ; ) {
3032
+ const start = scan.indexOf("/*", from);
3033
+ if (start === -1) break;
3034
+ const close = scan.indexOf("*/", start + 2);
3035
+ if (close === -1) break;
3036
+ from = close + 2;
3037
+ if (script[start + 2] === "*") out.push({ start, end: close + 2, text: script.slice(start, close + 2) });
3038
+ }
3039
+ return out;
3040
+ }
3041
+ function readJsDocTagType(comment, tag) {
3042
+ return readBracedType(comment, tag)?.type.trim() ?? null;
3043
+ }
3044
+ function readBracedType(comment, tag) {
3045
+ const tagMatch = new RegExp(`@${tag}\\s*\\{`).exec(comment);
3046
+ if (!tagMatch) return null;
3047
+ const open = tagMatch.index + tagMatch[0].length - 1;
3048
+ let depth = 0;
3049
+ for (let i = open; i < comment.length; i++) {
3050
+ const ch = comment[i];
3051
+ if (ch === "{") depth++;
3052
+ else if (ch === "}") {
3053
+ depth--;
3054
+ if (depth === 0) {
3055
+ const type = comment.slice(open + 1, i).replace(/\n\s*\*(?!\/)/g, "\n ");
3056
+ return { type, end: i + 1 };
3057
+ }
3058
+ }
3059
+ }
3060
+ return null;
3061
+ }
3062
+ function readJsDocTypeBefore(content, propIndex) {
3063
+ let end = propIndex;
3064
+ while (end > 0 && /\s/.test(content[end - 1])) end--;
3065
+ if (!content.startsWith("*/", end - 2)) return void 0;
3066
+ const start = content.lastIndexOf("/*", end - 2);
3067
+ if (start === -1 || content[start + 2] !== "*") return void 0;
3068
+ return readBracedType(content.slice(start, end), "type")?.type.trim();
3069
+ }
3070
+ function collectJsDocTypedefs(script, scan) {
3071
+ const typedefs = /* @__PURE__ */ new Map();
3072
+ for (const { text: comment } of findJsDocComments(script, scan)) {
3073
+ const braced = readBracedType(comment, "typedef");
3074
+ if (braced === null) continue;
3075
+ const name = /^\s*([$A-Za-z_][$\w]*)/.exec(comment.slice(braced.end));
3076
+ const type = parseJsDocType(braced.type);
3077
+ if (name && type !== null) typedefs.set(name[1], type);
3078
+ }
3079
+ return typedefs;
3080
+ }
3081
+ function flattenJsDocType(type, typedefs, depth = 0) {
3082
+ if (type.kind === "union") return type.members.flatMap((member) => flattenJsDocType(member, typedefs, depth));
3083
+ if (type.kind === "name" && depth < MAX_RESOLVE_DEPTH) {
3084
+ const resolved = typedefs.get(type.name);
3085
+ if (resolved !== void 0) return flattenJsDocType(resolved, typedefs, depth + 1);
3086
+ }
3087
+ return [type];
3088
+ }
3089
+ function jsDocTypeHint(type, typedefs = /* @__PURE__ */ new Map()) {
3090
+ const hints = /* @__PURE__ */ new Set();
3091
+ for (const member of flattenJsDocType(type, typedefs)) {
3092
+ if (member.kind === "array") hints.add("array");
3093
+ else if (member.kind === "object") hints.add("object");
3094
+ else if (member.kind === "name") {
3095
+ const lower2 = member.name.toLowerCase();
3096
+ if (lower2 === "string" || lower2 === "number" || lower2 === "boolean" || lower2 === "object") hints.add(lower2);
3097
+ else if (lower2 === "null" || lower2 === "undefined") hints.add("null");
3098
+ else if (lower2 === "array" || lower2 === "readonlyarray") hints.add("array");
3099
+ }
3100
+ }
3101
+ return hints.size === 0 ? void 0 : [...hints].sort().join("|");
3102
+ }
3103
+ function jsDocShape(type, typedefs) {
3104
+ const elements = [];
3105
+ const fields = [];
3106
+ for (const member of flattenJsDocType(type, typedefs)) {
3107
+ if (member.kind === "array") elements.push(member.element);
3108
+ else if (member.kind === "object") {
3109
+ for (const field of member.fields) {
3110
+ if (!fields.some((f) => f.name === field.name)) fields.push(field);
3111
+ }
3112
+ }
3113
+ }
3114
+ const element2 = elements.length === 0 ? null : elements.length === 1 ? elements[0] : { kind: "union", members: elements };
3115
+ return { element: element2, fields };
3116
+ }
3117
+
2825
3118
  // src/service/stateAnalyzer.ts
2826
3119
  var RESERVED_STREAMS_KEYS = /* @__PURE__ */ new Set(["$stream", "$streams"]);
2827
3120
  var RESERVED_COMMAND_TOKENS_KEY = "$commandTokens";
@@ -2839,6 +3132,7 @@ function analyzeStatePaths(scriptContent) {
2839
3132
  const pendingListKeys = [];
2840
3133
  const recursionSpec = specFromRecursionValue(topLevelProps.find((p) => p.name === RESERVED_RECURSION_KEY));
2841
3134
  const effectiveDescriptors = effectiveTopLevelDescriptors(topLevelProps);
3135
+ const typedefs = scriptContent.includes("@typedef") ? collectJsDocTypedefs(scriptContent, maskCommentsAndStrings(scriptContent)) : /* @__PURE__ */ new Map();
2842
3136
  for (const prop of topLevelProps) {
2843
3137
  if (prop.name.startsWith("$")) {
2844
3138
  collectReservedKeyPaths(prop, paths, pendingStreamValues, pendingListKeys);
@@ -2858,11 +3152,11 @@ function analyzeStatePaths(scriptContent) {
2858
3152
  }
2859
3153
  continue;
2860
3154
  }
2861
- pushDataPropertyPaths(prop, paths);
3155
+ pushDataPropertyPaths(prop, paths, typedefs);
2862
3156
  }
2863
3157
  for (const streamValue of pendingStreamValues) {
2864
3158
  if (paths.some((p) => p.path === streamValue.name && p.kind !== "eventToken")) continue;
2865
- pushDataPropertyPaths(streamValue, paths);
3159
+ pushDataPropertyPaths(streamValue, paths, typedefs);
2866
3160
  }
2867
3161
  for (const listKeyEntry of pendingListKeys) {
2868
3162
  pushListKeyPaths(listKeyEntry, paths);
@@ -3592,17 +3886,37 @@ function effectiveTopLevelDescriptors(props) {
3592
3886
  }
3593
3887
  return effective;
3594
3888
  }
3595
- function pushDataPropertyPaths(prop, paths) {
3596
- pushDataPropertyPathsAt(prop.name, prop, paths, 0);
3889
+ function pushDataPropertyPaths(prop, paths, typedefs) {
3890
+ pushDataPropertyPathsAt(prop.name, prop, paths, 0, typedefs);
3597
3891
  }
3598
- function pushDataPropertyPathsAt(path, prop, paths, depth) {
3892
+ function pushDataPropertyPathsAt(path, prop, paths, depth, typedefs) {
3599
3893
  paths.push({ path, kind: "data", typeHint: prop.typeHint, rawInitial: prop.value?.trim() });
3894
+ pushLiteralStructurePaths(path, prop, paths, depth, typedefs);
3895
+ if (prop.declaredType !== void 0) pushDeclaredShapePaths(path, prop.declaredType, paths, depth, typedefs);
3896
+ }
3897
+ function pushDeclaredShapePaths(path, type, paths, depth, typedefs) {
3898
+ const shape = jsDocShape(type, typedefs);
3899
+ if (shape.element !== null) {
3900
+ if (!hasPath(paths, `${path}.*`)) paths.push({ path: `${path}.*`, kind: "list" });
3901
+ if (!hasPath(paths, `${path}.length`)) paths.push({ path: `${path}.length`, kind: "data", typeHint: "number" });
3902
+ if (depth < MAX_OBJECT_NEST_DEPTH) pushDeclaredShapePaths(`${path}.*`, shape.element, paths, depth + 1, typedefs);
3903
+ }
3904
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
3905
+ for (const field of shape.fields) {
3906
+ const fieldPath = `${path}.${field.name}`;
3907
+ if (!hasPath(paths, fieldPath)) {
3908
+ paths.push(withHint({ path: fieldPath, kind: "data" }, jsDocTypeHint(field.type, typedefs)));
3909
+ }
3910
+ pushDeclaredShapePaths(fieldPath, field.type, paths, depth + 1, typedefs);
3911
+ }
3912
+ }
3913
+ function pushLiteralStructurePaths(path, prop, paths, depth, typedefs) {
3600
3914
  if (prop.value && isArrayLiteral(prop.value)) {
3601
3915
  paths.push({ path: `${path}.*`, kind: "list" });
3602
3916
  paths.push({ path: `${path}.length`, kind: "data", typeHint: "number" });
3603
3917
  if (depth >= MAX_OBJECT_NEST_DEPTH) return;
3604
3918
  for (const childProp of extractArrayElementDataProperties(prop.value)) {
3605
- pushDataPropertyPathsAt(`${path}.*.${childProp.name}`, childProp, paths, depth + 1);
3919
+ pushDataPropertyPathsAt(`${path}.*.${childProp.name}`, childProp, paths, depth + 1, typedefs);
3606
3920
  }
3607
3921
  return;
3608
3922
  }
@@ -3611,7 +3925,7 @@ function pushDataPropertyPathsAt(path, prop, paths, depth) {
3611
3925
  const childProps = parseTopLevelProperties(extractObjectContent(prop.value));
3612
3926
  for (const childProp of childProps) {
3613
3927
  if (childProp.kind !== "data") continue;
3614
- pushDataPropertyPathsAt(`${path}.${childProp.name}`, childProp, paths, depth + 1);
3928
+ pushDataPropertyPathsAt(`${path}.${childProp.name}`, childProp, paths, depth + 1, typedefs);
3615
3929
  }
3616
3930
  }
3617
3931
  }
@@ -3722,13 +4036,16 @@ function parseTopLevelProperties(objectContent, allowEmptyDataKeys = false) {
3722
4036
  if (propName !== void 0) {
3723
4037
  const valueStartIndex = match.index + match[0].length;
3724
4038
  const value = extractFullValue(objectContent, scan, valueStartIndex);
3725
- const jsdocType = extractJsDocType(objectContent, match.index);
4039
+ const jsdocText = readJsDocTypeBefore(objectContent, match.index);
4040
+ const declaredType = jsdocText === void 0 ? null : parseJsDocType(jsdocText);
4041
+ const jsdocType = declaredType !== null ? jsDocTypeHint(declaredType) : jsdocText === void 0 ? void 0 : normalizeJsDocType(jsdocText);
3726
4042
  const typeHint = jsdocType ?? inferTypeHint(value);
3727
4043
  props.push({
3728
4044
  name: propName,
3729
4045
  kind: "data",
3730
4046
  value,
3731
4047
  typeHint,
4048
+ ...declaredType !== null ? { declaredType } : {},
3732
4049
  nameStart: nameSpan[0],
3733
4050
  nameEnd: nameSpan[1],
3734
4051
  valueStart: valueStartIndex
@@ -3849,13 +4166,6 @@ function extractArrayElementDataProperties(value) {
3849
4166
  const objectContent = extractBracedContent(trimmed, scan, first);
3850
4167
  return parseTopLevelProperties(objectContent).filter((prop) => prop.kind === "data");
3851
4168
  }
3852
- function extractJsDocType(content, propIndex) {
3853
- const before = content.slice(Math.max(0, propIndex - 200), propIndex);
3854
- const jsdocMatch = before.match(/\/\*\*\s*@type\s*\{([^}]+)\}\s*\*\/\s*$/);
3855
- if (!jsdocMatch) return void 0;
3856
- const typeExpr = jsdocMatch[1].trim();
3857
- return normalizeJsDocType(typeExpr);
3858
- }
3859
4169
  function normalizeJsDocType(typeExpr) {
3860
4170
  const parts = typeExpr.split("|").map((p) => p.trim());
3861
4171
  const normalized = parts.map((p) => {
@@ -10596,6 +10906,10 @@ function resolveLocale(locale2) {
10596
10906
  if (locale2 === void 0 || locale2 === "" || /^ja\b|^ja[-_]/i.test(locale2) || locale2.toLowerCase() === "ja") return "ja";
10597
10907
  return "en";
10598
10908
  }
10909
+ function shortSeed(raw) {
10910
+ const flat = raw.replace(/\s+/g, " ");
10911
+ return flat.length <= 32 ? flat : `${flat.slice(0, 29)}\u2026`;
10912
+ }
10599
10913
  var JA_EXPECTED_LABEL = {
10600
10914
  array: "\u914D\u5217\u578B\u306E\u30D1\u30B9",
10601
10915
  boolean: "\u30D6\u30FC\u30EA\u30A2\u30F3\u578B",
@@ -10675,7 +10989,9 @@ var ja = {
10675
10989
  didYouMean: (c) => `\u3002\u3082\u3057\u304B\u3057\u3066: "${c}"`,
10676
10990
  none: () => `\u306A\u3057`,
10677
10991
  triggerSeededTruthy: (path) => `trigger \u30D0\u30A4\u30F3\u30C9\u5148 "${path}" \u304C true \u3067\u30B7\u30FC\u30C9\u3055\u308C\u3066\u3044\u307E\u3059\u3002trigger \u306F\u30A8\u30C3\u30B8\u691C\u51FA\u306A\u3057\uFF08truthy \u66F8\u304D\u8FBC\u307F\u3067\u5373\u767A\u706B\u30FBmanual \u3082\u30D0\u30A4\u30D1\u30B9\uFF09\u306E\u305F\u3081\u3001\u30D0\u30A4\u30F3\u30C9\u6642\u306B\u5373\u767A\u706B\u3057\u307E\u3059\u3002false \u3067\u30B7\u30FC\u30C9\u3057\u3066\u304F\u3060\u3055\u3044`,
10678
- storageSeedClobber: (path, raw) => `<wcs-storage> \u306E value \u30D0\u30A4\u30F3\u30C9\u5148 "${path}" \u304C ${raw} \u3067\u30B7\u30FC\u30C9\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u521D\u671F\u66F8\u304D\u623B\u3057\u304C\u4FDD\u5B58\u5024\u3092\u4E0A\u66F8\u304D\u3057\u307E\u3059 \u2014 undefined \u3067\u30B7\u30FC\u30C9\uFF08\`${path}: undefined\`\uFF09\u3059\u308B\u304B manual \u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044`,
10992
+ storageSeedClobber: (path, raw) => `<wcs-storage> \u306E value \u30D0\u30A4\u30F3\u30C9\u5148 "${path}" \u304C ${raw} \u3067\u30B7\u30FC\u30C9\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u8AAD\u307F\u8FBC\u307F\u306E\u305F\u3073\u306B\u521D\u671F\u66F8\u304D\u623B\u3057\u304C\u4FDD\u5B58\u5024\u3092\u4E0A\u66F8\u304D\u3057\u307E\u3059 \u2014 \u30D0\u30A4\u30F3\u30C9\u306B #init=element \u3092\u4ED8\u3051\u3066\uFF08\`value#init=element: ${path}\`\uFF09\u4FDD\u5B58\u5024\u3092\u52DD\u305F\u305B\u3001\u30B9\u30ED\u30C3\u30C8\u306F null\uFF08\u7A7A\u306E\u30AD\u30FC\u304C\u8AAD\u3080\u5024\uFF09\u3067\u30B7\u30FC\u30C9\u3057\u3066\u304F\u3060\u3055\u3044`,
10993
+ storageInitSeed: (path, raw) => `<wcs-storage> \u306E value \u30D0\u30A4\u30F3\u30C9\u5148 "${path}" \u304C ${shortSeed(raw)} \u3067\u30B7\u30FC\u30C9\u3055\u308C\u3066\u3044\u307E\u3059\u304C\u3001#init=element \u306F\u8981\u7D20\u306E\u5024\u3092\u3053\u306E\u30B9\u30ED\u30C3\u30C8\u3078\u5F15\u304D\u8FBC\u307F\u307E\u3059\u3002\u521D\u56DE\uFF08\u7A7A\u306E\u30AD\u30FC\uFF09\u306F null \u306A\u306E\u3067\u3001\u30B7\u30FC\u30C9\u306F null \u306B\u7F6E\u304D\u63DB\u308F\u308A\u307E\u3059 \u2014 null \u3067\u30B7\u30FC\u30C9\u3057\u3066\u8AAD\u3080\u5074\u3067\u65E2\u5B9A\u5024\u3092\u88DC\u3046\uFF08\`this.${path} ?? \u2026\`\uFF09\u304B\u3001null \u3092\u7121\u8996\u3059\u308B setter \u3092\u6301\u3064\u30A2\u30AF\u30BB\u30B5\u30DA\u30A2\u3092\u675F\u306D\u3066\u304F\u3060\u3055\u3044`,
10994
+ inputNestedWrite: (tag, input, path, example) => `<${tag}> \u306E\u5165\u529B "${input}" \u306B "${path}" \u3092\u675F\u306D\u3066\u3044\u307E\u3059\u304C\u3001\u30DA\u30FC\u30B8\u306F\u305D\u306E\u4E0B\uFF08"${example}"\uFF09\u3078\u66F8\u304D\u8FBC\u3093\u3067\u3044\u307E\u3059\u3002\u675F\u7E1B\u306F "${path}" \u81EA\u8EAB\u3078\u306E\u4EE3\u5165\u3067\u3057\u304B\u5F53\u3066\u76F4\u3055\u308C\u306A\u3044\u306E\u3067\u3001\u4E0B\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306F <${tag}> \u306B\u5C4A\u304D\u307E\u305B\u3093 \u2014 \u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u30D1\u30B9\u3067\u8AAD\u3093\u3067\u65B0\u3057\u3044\u5024\u3092\u8FD4\u3059 getter\uFF08\`this["${example}"]\`\u30FB\`$getAll\`\uFF09\u3092\u675F\u306D\u308B\u304B\u3001\u4E0B\u3078\u66F8\u304B\u305A\u306B "${path}" \u3092\u5DEE\u3057\u66FF\u3048\u3066\u304F\u3060\u3055\u3044`,
10679
10995
  devtoolsAfterState: () => `@wcstack/devtools/auto \u306F @wcstack/state/auto \u3088\u308A\u5148\u306B\u8AAD\u307F\u8FBC\u3093\u3067\u304F\u3060\u3055\u3044\uFF08\u5F8C\u3060\u3068\u914D\u7DDA\u53F0\u5E33\u304C\u30E9\u30A4\u30D6\u3067 captured \u3055\u308C\u307E\u305B\u3093\uFF09`,
10680
10996
  baseHrefMissing: () => `@wcstack/router \u3092\u4F7F\u3046 SPA \u306B\u306F <head> \u5185\u306E <base href="/"> \u304C\u5FC5\u8981\u3067\u3059\uFF08\u7121\u3044\u3068\u30C7\u30A3\u30FC\u30D7\u30EA\u30F3\u30AF\u3067 basename \u304C\u8AA4\u5C0E\u51FA\u3055\u308C\u307E\u3059\uFF09`,
10681
10997
  signalsDualEntry: () => `@wcstack/signals \u3068 @wcstack/signals/dom \u304C\u540C\u4E00\u30DA\u30FC\u30B8\u304B\u3089 import \u3055\u308C\u3066\u3044\u307E\u3059\u3002CDN \u3067\u306F\u5404\u30A8\u30F3\u30C8\u30EA\u304C\u81EA\u5DF1\u5B8C\u7D50\u30D0\u30F3\u30C9\u30EB\u306E\u305F\u3081\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u30B3\u30A2\u304C\u4E8C\u91CD\u5316\u3057\u3001\u5883\u754C\u3067\u53CD\u5FDC\u304C\u58CA\u308C\u307E\u3059 \u2014 \u3059\u3079\u3066 /dom \u30A8\u30F3\u30C8\u30EA\u304B\u3089 import \u3057\u3066\u304F\u3060\u3055\u3044`,
@@ -10861,7 +11177,9 @@ var en = {
10861
11177
  didYouMean: (c) => `. Did you mean "${c}"?`,
10862
11178
  none: () => `none`,
10863
11179
  triggerSeededTruthy: (path) => `The trigger-bound slot "${path}" is seeded with true. trigger has no edge detection (any truthy write fires, and it bypasses manual), so it fires immediately at bind. Seed it with false`,
10864
- storageSeedClobber: (path, raw) => `The <wcs-storage> value-bound slot "${path}" is seeded with ${raw}. The initial write-back overwrites the persisted value \u2014 seed it with undefined (\`${path}: undefined\`) or add manual`,
11180
+ storageSeedClobber: (path, raw) => `The <wcs-storage> value-bound slot "${path}" is seeded with ${raw}. The initial write-back overwrites the persisted value on every load \u2014 add #init=element to the binding (\`value#init=element: ${path}\`) so the stored value wins, and seed the slot with null (what an empty key loads as)`,
11181
+ storageInitSeed: (path, raw) => `The <wcs-storage> value-bound slot "${path}" is seeded with ${shortSeed(raw)}, but #init=element pulls the element's value into it. On a first visit (an empty key) that is null, so the seed is replaced by null \u2014 seed it with null and supply the default where you read it (\`this.${path} ?? \u2026\`), or bind an accessor pair whose setter ignores null`,
11182
+ inputNestedWrite: (tag, input, path, example) => `"${path}" is bound to the input "${input}" of <${tag}>, but the page writes below it ("${example}"). A binding is re-applied only when "${path}" itself is assigned, so those writes never reach <${tag}> \u2014 bind a getter that reads the fields by path (\`this["${example}"]\`, \`$getAll\`) and returns a new value, or replace "${path}" instead of writing below it`,
10865
11183
  devtoolsAfterState: () => `Load @wcstack/devtools/auto BEFORE @wcstack/state/auto (otherwise the wiring ledger is not captured live)`,
10866
11184
  baseHrefMissing: () => `An SPA using @wcstack/router needs <base href="/"> in <head> (without it, deep links misderive the basename)`,
10867
11185
  signalsDualEntry: () => `Both @wcstack/signals and @wcstack/signals/dom are imported on this page. On a CDN each entry is a self-contained bundle, so the reactive core is duplicated and reactivity breaks at the seam \u2014 import everything from the single /dom entry`,
@@ -11806,18 +12124,24 @@ function validateStateTypes(html, stateTagName = "wcs-state", locale2) {
11806
12124
  }
11807
12125
  return diagnostics;
11808
12126
  }
12127
+ var PROPERTY_AFTER_COMMENT = /^\s*(?:"([^"]+)"|'([^']+)'|(\w+))\s*:\s*/;
11809
12128
  function findJsDocTypedProperties(script) {
11810
12129
  const results = [];
11811
- const regex = /\/\*\*\s*@type\s*\{([^}]+)\}\s*\*\/\s*(?:"([^"]+)"|'([^']+)'|(\w+))\s*:\s*/g;
11812
- let match;
11813
- while ((match = regex.exec(script)) !== null) {
11814
- const rawType = match[1].trim();
11815
- const name = match[2] ?? match[3] ?? match[4];
11816
- const valueStart = match.index + match[0].length;
12130
+ const scan = maskCommentsAndStrings(script);
12131
+ const typedefs = script.includes("@typedef") ? collectJsDocTypedefs(script, scan) : /* @__PURE__ */ new Map();
12132
+ for (const comment of findJsDocComments(script, scan)) {
12133
+ const typeText = readJsDocTagType(comment.text, "type");
12134
+ if (typeText === null) continue;
12135
+ const match = PROPERTY_AFTER_COMMENT.exec(script.slice(comment.end));
12136
+ if (!match) continue;
12137
+ const rawType = typeText.replace(/\s+/g, " ");
12138
+ const name = match[1] ?? match[2] ?? match[3];
12139
+ const valueStart = comment.end + match[0].length;
11817
12140
  const valueText = extractValue(script, valueStart);
11818
12141
  const valueType = inferValueType(valueText);
11819
12142
  if (valueType) {
11820
- const declaredTypes = rawType.split("|").map((t) => normalizeType(t.trim()));
12143
+ const declaredTypes = declaredTypesOf(rawType, typedefs);
12144
+ if (declaredTypes === null) continue;
11821
12145
  results.push({
11822
12146
  name,
11823
12147
  rawType,
@@ -11830,6 +12154,20 @@ function findJsDocTypedProperties(script) {
11830
12154
  }
11831
12155
  return results;
11832
12156
  }
12157
+ var ANY_TYPES = /* @__PURE__ */ new Set(["any", "unknown", "*"]);
12158
+ function declaredTypesOf(rawType, typedefs) {
12159
+ if (rawType.split("|").some((t) => ANY_TYPES.has(t.trim().toLowerCase()))) return null;
12160
+ const parsed = parseJsDocType(rawType);
12161
+ if (parsed === null) return rawType.split("|").map((t) => normalizeType(t.trim()));
12162
+ const out = [];
12163
+ for (const member of flattenJsDocType(parsed, typedefs)) {
12164
+ if (member.kind === "unknown") return null;
12165
+ if (member.kind === "array") out.push("array");
12166
+ else if (member.kind === "object") out.push("object");
12167
+ else if (member.kind === "name") out.push(normalizeType(member.name));
12168
+ }
12169
+ return out;
12170
+ }
11833
12171
  function extractValue(script, start) {
11834
12172
  let depth = 0;
11835
12173
  let inString = null;
@@ -13863,6 +14201,24 @@ function validateBindingAgainstContract(tagName, contract, parsed, property, sta
13863
14201
  });
13864
14202
  }
13865
14203
  }
14204
+ if (tagName === "wcs-storage" && property === "value" && parsed.path && /(?:^|,)init=element\b/.test(modifiers)) {
14205
+ const cand = findDataSlot(getPaths(), parsed.path);
14206
+ if (cand?.rawInitial !== void 0 && isNonNullLiteralSeed(cand.rawInitial)) {
14207
+ diagnostics.push({
14208
+ code: WcsDiagnosticCode.StorageInitSeed,
14209
+ start,
14210
+ end,
14211
+ severity: "warning",
14212
+ tag: tagName,
14213
+ statePath: parsed.path,
14214
+ message: msgs.storageInitSeed(parsed.path, cand.rawInitial)
14215
+ });
14216
+ }
14217
+ }
14218
+ }
14219
+ function isNonNullLiteralSeed(raw) {
14220
+ const v = raw.trim();
14221
+ return /^(?:["'`[{]|-?\d)/.test(v) || v === "true" || v === "false";
13866
14222
  }
13867
14223
  function findDataSlot(paths, path) {
13868
14224
  return paths.find((c) => c.kind === "data" && c.path === path);
@@ -15506,15 +15862,63 @@ function validateGetterUntrackedReads(script, scriptStart, nestedWriteRoots, loc
15506
15862
  }
15507
15863
  return out;
15508
15864
  }
15865
+ var NESTED_WRITE_INPUTS = {
15866
+ "wcs-storage": { inputs: ["value"], exemptWhenManual: true }
15867
+ };
15868
+ var MANUAL_ATTRIBUTE = /(?:^|\s)manual(?:\s|=|$)/i;
15869
+ function validateInputNestedWrites(html, stateTagName, bindAttrName, nestedWriteRoots, locale2) {
15870
+ const tags = findBuiltinTagOccurrences(html).filter((occ) => {
15871
+ const target = NESTED_WRITE_INPUTS[occ.tagName];
15872
+ return target !== void 0 && !(target.exemptWhenManual && MANUAL_ATTRIBUTE.test(occ.attrsText));
15873
+ }).map((occ) => ({ tagName: occ.tagName, start: occ.tagStart, end: occ.attrsStart + occ.attrsText.length }));
15874
+ if (tags.length === 0) return [];
15875
+ const msgs = getMessages(locale2);
15876
+ const out = [];
15877
+ const index = buildReferenceIndex(html, { bindAttribute: bindAttrName, stateTagName });
15878
+ for (const occ of index.occurrences) {
15879
+ if (occ.kind !== "path" || occ.source !== "attribute" || occ.bindingType !== "prop" || occ.propName === null) continue;
15880
+ if (occ.path.startsWith(".")) continue;
15881
+ const at2 = occ.exprRange.start;
15882
+ const tag = tags.find((t) => t.start <= at2 && at2 <= t.end);
15883
+ if (tag === void 0) continue;
15884
+ const input = occ.propName.startsWith(".") ? occ.propName.slice(1) : occ.propName;
15885
+ if (!NESTED_WRITE_INPUTS[tag.tagName].inputs.includes(input)) continue;
15886
+ const example = nestedWriteRoots().get(occ.path);
15887
+ if (example === void 0) continue;
15888
+ out.push({
15889
+ code: WcsDiagnosticCode.InputNestedWrite,
15890
+ start: occ.pathRange.start,
15891
+ end: occ.pathRange.end,
15892
+ severity: "warning",
15893
+ tag: tag.tagName,
15894
+ statePath: occ.path,
15895
+ message: msgs.inputNestedWrite(tag.tagName, input, occ.path, example)
15896
+ });
15897
+ }
15898
+ return out;
15899
+ }
15509
15900
  var TWO_WAY_PROPS = /* @__PURE__ */ new Set(["value", "checked"]);
15510
15901
  var BRACKET_WRITE = new RegExp(`${ROOT_BRACKET}${ASSIGN_TAIL}`, "g");
15511
15902
  var PRE_BRACKET_INCDEC2 = new RegExp(`${PRE_INCDEC}${ROOT_BRACKET}`, "g");
15903
+ var TEMPLATE_BRACKET = String.raw`\bthis\s*(?:\?\.)?\s*\[\s*` + "`([^`]*)`" + String.raw`\s*\]`;
15904
+ var TEMPLATE_BRACKET_WRITE = new RegExp(`${TEMPLATE_BRACKET}${ASSIGN_TAIL}`, "g");
15905
+ var PRE_TEMPLATE_BRACKET_INCDEC = new RegExp(`${PRE_INCDEC}${TEMPLATE_BRACKET}`, "g");
15906
+ var TEMPLATE_KEY_PATH = /^[A-Za-z_][\w]*(?:\.(?:\*|[\w]+))*$/;
15907
+ function templateKeyPath(raw) {
15908
+ if (raw.includes("`")) return null;
15909
+ const path = raw.replace(/\$\{[^{}]*\}/g, "*");
15910
+ return TEMPLATE_KEY_PATH.test(path) ? path : null;
15911
+ }
15512
15912
  function collectNestedWriteRoots(html, stateTagName, bindAttrName, blocks) {
15513
- const roots = /* @__PURE__ */ new Set();
15913
+ const roots = /* @__PURE__ */ new Map();
15514
15914
  const addPrefixes = (path, inclusive) => {
15515
15915
  const segments = path.split(".");
15516
15916
  const last = inclusive ? segments.length : segments.length - 1;
15517
- for (let i = 1; i <= last; i++) roots.add(segments.slice(0, i).join("."));
15917
+ const example = inclusive ? `${path}.*` : path;
15918
+ for (let i = 1; i <= last; i++) {
15919
+ const prefix = segments.slice(0, i).join(".");
15920
+ if (!roots.has(prefix)) roots.set(prefix, example);
15921
+ }
15518
15922
  };
15519
15923
  const tags = findBuiltinTagOccurrences(html).map((occ) => ({
15520
15924
  contract: BUILTIN_TAGS[occ.tagName],
@@ -15523,23 +15927,29 @@ function collectNestedWriteRoots(html, stateTagName, bindAttrName, blocks) {
15523
15927
  }));
15524
15928
  const index = buildReferenceIndex(html, { bindAttribute: bindAttrName, stateTagName });
15525
15929
  for (const occ of index.occurrences) {
15526
- if (occ.kind !== "path" || occ.path.startsWith(".") || occ.source !== "attribute") continue;
15930
+ if (occ.kind !== "path" || occ.source !== "attribute") continue;
15931
+ let path = occ.path;
15932
+ if (path.startsWith(".")) {
15933
+ const list = getRowShorthandForPath(html, occ.pathRange.start, bindAttrName) === null ? null : getResolvedForListPath(html, occ.pathRange.start, bindAttrName);
15934
+ if (list === null) continue;
15935
+ path = path === "." ? `${list}.*` : `${list}.*.${path.slice(1)}`;
15936
+ }
15527
15937
  if (occ.bindingType === "spread") {
15528
- addPrefixes(occ.path, true);
15938
+ addPrefixes(path, true);
15529
15939
  continue;
15530
15940
  }
15531
15941
  if (occ.bindingType === "radio" || occ.bindingType === "checkbox") {
15532
- addPrefixes(occ.path, false);
15942
+ addPrefixes(path, false);
15533
15943
  continue;
15534
15944
  }
15535
15945
  if (occ.bindingType !== "prop" || occ.propName === null) continue;
15536
15946
  if (TWO_WAY_PROPS.has(occ.propName)) {
15537
- addPrefixes(occ.path, false);
15947
+ addPrefixes(path, false);
15538
15948
  continue;
15539
15949
  }
15540
15950
  const at2 = occ.exprRange.start;
15541
15951
  const tag = tags.find((t) => t.start <= at2 && at2 <= t.end);
15542
- if (tag?.contract?.hasWcBindable && tag.contract.properties.includes(occ.propName)) addPrefixes(occ.path, false);
15952
+ if (tag?.contract?.hasWcBindable && tag.contract.properties.includes(occ.propName)) addPrefixes(path, false);
15543
15953
  }
15544
15954
  for (const block of blocks) {
15545
15955
  if (block.mountPath !== null) addPrefixes(block.mountPath, true);
@@ -15552,6 +15962,15 @@ function collectNestedWriteRoots(html, stateTagName, bindAttrName, blocks) {
15552
15962
  addPrefixes(block.content.slice(keyStart, keyStart + match[1].length), false);
15553
15963
  }
15554
15964
  }
15965
+ for (const regex of [TEMPLATE_BRACKET_WRITE, PRE_TEMPLATE_BRACKET_INCDEC]) {
15966
+ regex.lastIndex = 0;
15967
+ let match;
15968
+ while ((match = regex.exec(scan)) !== null) {
15969
+ const keyStart = match.index + match[0].indexOf("`") + 1;
15970
+ const path = templateKeyPath(block.content.slice(keyStart, keyStart + match[1].length));
15971
+ if (path !== null) addPrefixes(path, false);
15972
+ }
15973
+ }
15555
15974
  API_CALL.lastIndex = 0;
15556
15975
  let call;
15557
15976
  while ((call = API_CALL.exec(scan)) !== null) {
@@ -15667,6 +16086,7 @@ function validateSemantics(html, stateTagName = "wcs-state", locale2, bindAttrNa
15667
16086
  out.push(...validateGetterCycles(block.content, block.contentStart, locale2));
15668
16087
  out.push(...validateGetterUntrackedReads(block.content, block.contentStart, getNestedWriteRoots, locale2));
15669
16088
  }
16089
+ out.push(...validateInputNestedWrites(html, stateTagName, bindAttrName, getNestedWriteRoots, locale2));
15670
16090
  out.push(...validateUpdatedCallbackDemand(html, stateTagName, bindAttrName, locale2));
15671
16091
  return out;
15672
16092
  }
@@ -573,7 +573,7 @@ function loadSchemaCore() {
573
573
  return cached;
574
574
  }
575
575
 
576
- var version = "4.0.1";
576
+ var version = "4.0.2";
577
577
  var pkg = {
578
578
  version: version};
579
579
 
package/dist/wcs-tsc.mjs CHANGED
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { dirname, join, isAbsolute, resolve, basename } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
- var version = "4.0.1";
8
+ var version = "4.0.2";
9
9
  var pkg = {
10
10
  version: version};
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/typescript",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
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",