@wcstack/state 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/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +50 -3
- package/dist/index.esm.js +364 -61
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.esm.js +54 -1
- package/dist/parser.d.ts +115 -0
- package/dist/parser.esm.js +1527 -0
- package/dist/wcs-manifest.json +29 -1
- package/package.json +5 -1
package/dist/index.esm.js
CHANGED
|
@@ -113,6 +113,38 @@ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
|
|
|
113
113
|
const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
|
|
114
114
|
const STATE_NAME_SEPARATOR = '@'; // path と @stateName の区切り
|
|
115
115
|
const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
116
|
+
// 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
|
|
117
|
+
// フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
|
|
118
|
+
// 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
|
|
119
|
+
// この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
|
|
120
|
+
// (docs/static-wiring-dx-design.md §2-2)。
|
|
121
|
+
const MODIFIER_PREVENT = 'prevent';
|
|
122
|
+
const MODIFIER_STOP = 'stop';
|
|
123
|
+
const MODIFIER_READONLY = 'ro';
|
|
124
|
+
const MODIFIER_FLAGS = Object.freeze([
|
|
125
|
+
MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
|
|
126
|
+
]);
|
|
127
|
+
const MODIFIER_KEY_INIT = 'init';
|
|
128
|
+
const MODIFIER_KEY_SYNC = 'sync';
|
|
129
|
+
const MODIFIER_KEYS = Object.freeze([
|
|
130
|
+
MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
|
|
131
|
+
]);
|
|
132
|
+
// bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
|
|
133
|
+
// 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
|
|
134
|
+
// apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
|
|
135
|
+
// キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
|
|
136
|
+
// manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
|
|
137
|
+
const ELSE_KEYWORD = 'else';
|
|
138
|
+
const SPREAD_PROP = '...';
|
|
139
|
+
const EVENT_PROP_PREFIX = 'on';
|
|
140
|
+
const EVENT_TOKEN_NAMESPACE = 'eventToken';
|
|
141
|
+
const COMMAND_NAMESPACE = 'command';
|
|
142
|
+
const CLASS_NAMESPACE = 'class';
|
|
143
|
+
const ATTR_NAMESPACE = 'attr';
|
|
144
|
+
const STYLE_NAMESPACE = 'style';
|
|
145
|
+
// リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
|
|
146
|
+
// manifest.syntax.indexParam で公開される。
|
|
147
|
+
const INDEX_PARAM_PREFIX = '$';
|
|
116
148
|
/**
|
|
117
149
|
* stackIndexByIndexName
|
|
118
150
|
* インデックス名からスタックインデックスへのマッピング
|
|
@@ -124,7 +156,7 @@ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
|
124
156
|
*/
|
|
125
157
|
const tmpIndexByIndexName = {};
|
|
126
158
|
for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
|
|
127
|
-
tmpIndexByIndexName[
|
|
159
|
+
tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
|
|
128
160
|
}
|
|
129
161
|
const INDEX_BY_INDEX_NAME = Object.freeze(tmpIndexByIndexName);
|
|
130
162
|
const NO_SET_TIMEOUT = 60 * 1000; // 1分
|
|
@@ -633,6 +665,78 @@ function setBindingsByNode(node, bindings) {
|
|
|
633
665
|
bindingsByNode.set(node, bindings);
|
|
634
666
|
}
|
|
635
667
|
|
|
668
|
+
/**
|
|
669
|
+
* errorGuidance.ts — エラーメッセージへの self-fix 誘導(GTM 2-5 /
|
|
670
|
+
* docs/static-wiring-dx-design.md §3)。
|
|
671
|
+
*
|
|
672
|
+
* コンソールは「書き手(人間・AI とも)が誤った瞬間に必ず読む面」なので、
|
|
673
|
+
* (a) did-you-mean 候補 (b) lint への誘導 をエラーメッセージ自体に埋め込む。
|
|
674
|
+
* ここの関数は全て**エラーパスでのみ**呼ばれる — 正常系のコストはゼロ。
|
|
675
|
+
* auto.min.js に同梱されるため文字列は最小限に保つ(エラーパス専用モジュールの
|
|
676
|
+
* 遅延 import は `src/auto.ts` の SRI 自己完結制約で不可)。
|
|
677
|
+
*
|
|
678
|
+
* 診断 code の語彙はコンソール → lint → IDE の三面で共有する:
|
|
679
|
+
* メッセージ先頭の `[wcs/...]` は wcstack-intellisense / @wcstack/lint の
|
|
680
|
+
* 安定診断 code(packages/vscode-wcs/src/core/diagnostics.ts)と同一。
|
|
681
|
+
*/
|
|
682
|
+
/** 挿入・削除・置換の編集距離。長さ差が max を超えたら早期に max+1 を返す。 */
|
|
683
|
+
function editDistance(a, b, max) {
|
|
684
|
+
if (Math.abs(a.length - b.length) > max) {
|
|
685
|
+
return max + 1;
|
|
686
|
+
}
|
|
687
|
+
const prev = new Array(b.length + 1);
|
|
688
|
+
const curr = new Array(b.length + 1);
|
|
689
|
+
for (let j = 0; j <= b.length; j++) {
|
|
690
|
+
prev[j] = j;
|
|
691
|
+
}
|
|
692
|
+
for (let i = 1; i <= a.length; i++) {
|
|
693
|
+
curr[0] = i;
|
|
694
|
+
for (let j = 1; j <= b.length; j++) {
|
|
695
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
696
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
697
|
+
}
|
|
698
|
+
for (let j = 0; j <= b.length; j++) {
|
|
699
|
+
prev[j] = curr[j];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return prev[b.length];
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* 候補集合から編集距離 2 以内の最近傍を探し、` Did you mean "<best>"?` を返す。
|
|
706
|
+
* 該当なしは空文字。規準(距離 2・同距離は先勝ち・大小文字は畳んで比較)は
|
|
707
|
+
* lint の did-you-mean(ioNodeValidator の suggestion)と同じ — 三面で提案が
|
|
708
|
+
* 割れないように揃えている。動的キー等で候補が列挙できないサイトでは呼ばない
|
|
709
|
+
* = 誘導文のみに縮退(設計 §3 の縮退)。
|
|
710
|
+
*/
|
|
711
|
+
function didYouMean(input, candidates) {
|
|
712
|
+
// 空入力(`a|` の末尾パイプ等)に短い候補を提案しても無意味なので出さない。
|
|
713
|
+
if (input.length === 0) {
|
|
714
|
+
return "";
|
|
715
|
+
}
|
|
716
|
+
const folded = input.toLowerCase();
|
|
717
|
+
let best = null;
|
|
718
|
+
let bestDistance = 3;
|
|
719
|
+
for (const candidate of candidates) {
|
|
720
|
+
const distance = editDistance(folded, candidate.toLowerCase(), 2);
|
|
721
|
+
if (distance < bestDistance) {
|
|
722
|
+
best = candidate;
|
|
723
|
+
bestDistance = distance;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return best !== null ? ` Did you mean "${best}"?` : "";
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* lint への誘導(誘導付きメッセージ共通の一文)。
|
|
730
|
+
* **lint が実際にそのケースを検出するサイトにだけ付ける** — 検出しないケースに
|
|
731
|
+
* 付けると「エラー → lint 実行 → clean」の空振りで検証ループの信頼を毀損する。
|
|
732
|
+
* 現在 lint 未検出のため付けないもの: DCC 宣言・watch の空キー / Object.prototype
|
|
733
|
+
* 継承名 / ワイルドカード深度超過。
|
|
734
|
+
* なお hint 付きサイト内でも被覆は部分的でありうる(例: `$watch: ident` の実体が
|
|
735
|
+
* 非オブジェクトだった場合、ランタイムは評価後の値で raise するが lint は宣言 shape
|
|
736
|
+
* から断定できず沈黙する)。サイト粒度の hint ではこの残余は構造的に避けられない。
|
|
737
|
+
*/
|
|
738
|
+
const LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
|
|
739
|
+
|
|
636
740
|
const STRUCTURAL_BINDING_TYPE_SET = new Set([
|
|
637
741
|
"if",
|
|
638
742
|
"elseif",
|
|
@@ -1558,7 +1662,8 @@ const builtinFiltersByFilterIOType = {
|
|
|
1558
1662
|
const builtinFilterFn = (name, options) => (filters) => {
|
|
1559
1663
|
const filter = filters[name];
|
|
1560
1664
|
if (!filter) {
|
|
1561
|
-
|
|
1665
|
+
// lint の wcs/filter-unknown と同じ語彙・同じ did-you-mean 規準(三面同語彙)。
|
|
1666
|
+
raiseError(`[wcs/filter-unknown] filter not found: ${name}.${didYouMean(name, Object.keys(filters))}${LINT_HINT}`);
|
|
1562
1667
|
}
|
|
1563
1668
|
return filter(options);
|
|
1564
1669
|
};
|
|
@@ -1779,11 +1884,11 @@ function parseBindTextsForElement(bindText) {
|
|
|
1779
1884
|
}
|
|
1780
1885
|
const propPart = bindText.slice(0, separatorIndex).trim();
|
|
1781
1886
|
const statePart = bindText.slice(separatorIndex + 1).trim();
|
|
1782
|
-
if (propPart ===
|
|
1887
|
+
if (propPart === ELSE_KEYWORD) {
|
|
1783
1888
|
const pathInfo = getPathInfo('#else');
|
|
1784
1889
|
return {
|
|
1785
|
-
propName:
|
|
1786
|
-
propSegments: [
|
|
1890
|
+
propName: ELSE_KEYWORD,
|
|
1891
|
+
propSegments: [ELSE_KEYWORD],
|
|
1787
1892
|
propModifiers: [],
|
|
1788
1893
|
statePathName: '#else',
|
|
1789
1894
|
statePathInfo: pathInfo,
|
|
@@ -1793,7 +1898,7 @@ function parseBindTextsForElement(bindText) {
|
|
|
1793
1898
|
bindingType: 'else',
|
|
1794
1899
|
};
|
|
1795
1900
|
}
|
|
1796
|
-
else if (propPart ===
|
|
1901
|
+
else if (propPart === SPREAD_PROP) {
|
|
1797
1902
|
const stateResult = parseStatePart(statePart);
|
|
1798
1903
|
if (stateResult.outFilters.length > 0) {
|
|
1799
1904
|
raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets.`);
|
|
@@ -1802,8 +1907,8 @@ function parseBindTextsForElement(bindText) {
|
|
|
1802
1907
|
raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
|
|
1803
1908
|
}
|
|
1804
1909
|
return {
|
|
1805
|
-
propName:
|
|
1806
|
-
propSegments: [
|
|
1910
|
+
propName: SPREAD_PROP,
|
|
1911
|
+
propSegments: [SPREAD_PROP],
|
|
1807
1912
|
propModifiers: [],
|
|
1808
1913
|
inFilters: [],
|
|
1809
1914
|
...stateResult,
|
|
@@ -1830,14 +1935,14 @@ function parseBindTextsForElement(bindText) {
|
|
|
1830
1935
|
const propResult = parsePropPart(propPart);
|
|
1831
1936
|
// eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
|
|
1832
1937
|
// 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
|
|
1833
|
-
if (propResult.propSegments[0] ===
|
|
1938
|
+
if (propResult.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
1834
1939
|
return {
|
|
1835
1940
|
...propResult,
|
|
1836
1941
|
...stateResult,
|
|
1837
1942
|
bindingType: 'event',
|
|
1838
1943
|
};
|
|
1839
1944
|
}
|
|
1840
|
-
if (propResult.propSegments[0].startsWith(
|
|
1945
|
+
if (propResult.propSegments[0].startsWith(EVENT_PROP_PREFIX)) {
|
|
1841
1946
|
return {
|
|
1842
1947
|
...propResult,
|
|
1843
1948
|
...stateResult,
|
|
@@ -1857,7 +1962,9 @@ function parseBindTextsForElement(bindText) {
|
|
|
1857
1962
|
if (results.length > 1) {
|
|
1858
1963
|
const isIncludeSingleBinding = results.some(r => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
|
|
1859
1964
|
if (isIncludeSingleBinding) {
|
|
1860
|
-
|
|
1965
|
+
// lint 側の単独バインディング検査(bindingValidator の structuralMustBeSingle)が
|
|
1966
|
+
// 同じケースを検出するため誘導を付ける(三面同語彙)。
|
|
1967
|
+
raiseError(`[wcs/template-syntax] Invalid bindText: "${bindText}". 'if', 'elseif', 'else', and 'for' bindings must be single binding. Put the structural binding alone in its own data-wcs (e.g. <template data-wcs="for: items">).${LINT_HINT}`);
|
|
1861
1968
|
}
|
|
1862
1969
|
}
|
|
1863
1970
|
return results;
|
|
@@ -2733,8 +2840,8 @@ function getHandlerKey$3(binding, eventName) {
|
|
|
2733
2840
|
function getEventName$2(binding) {
|
|
2734
2841
|
let eventName = 'input';
|
|
2735
2842
|
for (const modifier of binding.propModifiers) {
|
|
2736
|
-
if (modifier.startsWith(
|
|
2737
|
-
eventName = modifier.slice(
|
|
2843
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
2844
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
2738
2845
|
}
|
|
2739
2846
|
}
|
|
2740
2847
|
return eventName;
|
|
@@ -2789,7 +2896,7 @@ const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (e
|
|
|
2789
2896
|
});
|
|
2790
2897
|
};
|
|
2791
2898
|
function attachCheckboxEventHandler(binding) {
|
|
2792
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2899
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2793
2900
|
const eventName = getEventName$2(binding);
|
|
2794
2901
|
const key = getHandlerKey$3(binding, eventName);
|
|
2795
2902
|
let checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2804,7 +2911,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
2804
2911
|
return false;
|
|
2805
2912
|
}
|
|
2806
2913
|
function detachCheckboxEventHandler(binding) {
|
|
2807
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2914
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2808
2915
|
const eventName = getEventName$2(binding);
|
|
2809
2916
|
const key = getHandlerKey$3(binding, eventName);
|
|
2810
2917
|
const checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2987,7 +3094,7 @@ function getWcBindable$1(element) {
|
|
|
2987
3094
|
return readBindableDeclaration(element);
|
|
2988
3095
|
}
|
|
2989
3096
|
function attachEventTokenHandler(binding) {
|
|
2990
|
-
if (binding.propSegments[0] !==
|
|
3097
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
2991
3098
|
return false;
|
|
2992
3099
|
}
|
|
2993
3100
|
const element = binding.node;
|
|
@@ -3014,16 +3121,16 @@ function attachEventTokenHandler(binding) {
|
|
|
3014
3121
|
}
|
|
3015
3122
|
const propDesc = bindable.knownProperties.get(propertyName);
|
|
3016
3123
|
if (typeof propDesc === "undefined") {
|
|
3017
|
-
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}
|
|
3124
|
+
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.${didYouMean(propertyName, bindable.knownProperties.keys())}`);
|
|
3018
3125
|
}
|
|
3019
3126
|
const eventName = propDesc.event;
|
|
3020
3127
|
const tokenName = binding.statePathName;
|
|
3021
3128
|
const stateName = binding.stateName;
|
|
3022
3129
|
const modifiers = binding.propModifiers;
|
|
3023
3130
|
const handler = (event) => {
|
|
3024
|
-
if (modifiers.includes(
|
|
3131
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
3025
3132
|
event.preventDefault();
|
|
3026
|
-
if (modifiers.includes(
|
|
3133
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
3027
3134
|
event.stopPropagation();
|
|
3028
3135
|
// state は発火時の live root から解決する(attach 時は detached の可能性があるため)。
|
|
3029
3136
|
const rootNode = element.getRootNode();
|
|
@@ -3032,7 +3139,8 @@ function attachEventTokenHandler(binding) {
|
|
|
3032
3139
|
raiseError(`State element with name "${stateName}" not found for eventToken handler.`);
|
|
3033
3140
|
}
|
|
3034
3141
|
if (!stateElement.eventTokenNames.has(tokenName)) {
|
|
3035
|
-
|
|
3142
|
+
// lint も同じケースを wcs/token-undeclared で検出する(三面同語彙)。
|
|
3143
|
+
raiseError(`[wcs/token-undeclared] eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".${didYouMean(tokenName, stateElement.eventTokenNames)}${LINT_HINT}`);
|
|
3036
3144
|
}
|
|
3037
3145
|
const loopContext = getLoopContextByNode(element);
|
|
3038
3146
|
stateElement.createStateAsync("writable", async (state) => {
|
|
@@ -3052,7 +3160,7 @@ function attachEventTokenHandler(binding) {
|
|
|
3052
3160
|
return true;
|
|
3053
3161
|
}
|
|
3054
3162
|
function detachEventTokenHandler(binding) {
|
|
3055
|
-
if (binding.propSegments[0] !==
|
|
3163
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
3056
3164
|
return false;
|
|
3057
3165
|
}
|
|
3058
3166
|
const listener = listenerByBinding.get(binding);
|
|
@@ -3105,13 +3213,13 @@ const handlerByHandlerKey$2 = new Map();
|
|
|
3105
3213
|
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
3106
3214
|
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
3107
3215
|
function getHandlerKey$2(binding) {
|
|
3108
|
-
const modifierKey = binding.propModifiers.filter(m => m ===
|
|
3216
|
+
const modifierKey = binding.propModifiers.filter(m => m === MODIFIER_PREVENT || m === MODIFIER_STOP).sort().join(',');
|
|
3109
3217
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
3110
3218
|
}
|
|
3111
3219
|
const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
|
|
3112
|
-
if (modifiers.includes(
|
|
3220
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
3113
3221
|
event.preventDefault();
|
|
3114
|
-
if (modifiers.includes(
|
|
3222
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
3115
3223
|
event.stopPropagation();
|
|
3116
3224
|
const node = event.target;
|
|
3117
3225
|
const rootNode = node.getRootNode();
|
|
@@ -3145,7 +3253,7 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
3145
3253
|
});
|
|
3146
3254
|
};
|
|
3147
3255
|
function attachEventHandler(binding) {
|
|
3148
|
-
if (!binding.propName.startsWith(
|
|
3256
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
3149
3257
|
return false;
|
|
3150
3258
|
}
|
|
3151
3259
|
const key = getHandlerKey$2(binding);
|
|
@@ -3160,7 +3268,7 @@ function attachEventHandler(binding) {
|
|
|
3160
3268
|
return true;
|
|
3161
3269
|
}
|
|
3162
3270
|
function detachEventHandler(binding) {
|
|
3163
|
-
if (!binding.propName.startsWith(
|
|
3271
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
3164
3272
|
return false;
|
|
3165
3273
|
}
|
|
3166
3274
|
const key = getHandlerKey$2(binding);
|
|
@@ -3189,8 +3297,8 @@ function getHandlerKey$1(binding, eventName) {
|
|
|
3189
3297
|
function getEventName$1(binding) {
|
|
3190
3298
|
let eventName = 'input';
|
|
3191
3299
|
for (const modifier of binding.propModifiers) {
|
|
3192
|
-
if (modifier.startsWith(
|
|
3193
|
-
eventName = modifier.slice(
|
|
3300
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3301
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
3194
3302
|
}
|
|
3195
3303
|
}
|
|
3196
3304
|
return eventName;
|
|
@@ -3226,7 +3334,7 @@ const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (even
|
|
|
3226
3334
|
});
|
|
3227
3335
|
};
|
|
3228
3336
|
function attachRadioEventHandler(binding) {
|
|
3229
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3337
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3230
3338
|
const eventName = getEventName$1(binding);
|
|
3231
3339
|
const key = getHandlerKey$1(binding, eventName);
|
|
3232
3340
|
let radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -3241,7 +3349,7 @@ function attachRadioEventHandler(binding) {
|
|
|
3241
3349
|
return false;
|
|
3242
3350
|
}
|
|
3243
3351
|
function detachRadioEventHandler(binding) {
|
|
3244
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3352
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3245
3353
|
const eventName = getEventName$1(binding);
|
|
3246
3354
|
const key = getHandlerKey$1(binding, eventName);
|
|
3247
3355
|
const radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -3480,10 +3588,10 @@ function getEventName(binding) {
|
|
|
3480
3588
|
eventName = propDesc.event;
|
|
3481
3589
|
}
|
|
3482
3590
|
}
|
|
3483
|
-
// 3.modifier
|
|
3591
|
+
// 3.modifier(`#onchange` 等 — `on` + イベント名の修飾子形。README「Modifiers」参照)
|
|
3484
3592
|
for (const modifier of binding.propModifiers) {
|
|
3485
|
-
if (modifier.startsWith(
|
|
3486
|
-
eventName = modifier.slice(
|
|
3593
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3594
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
3487
3595
|
}
|
|
3488
3596
|
}
|
|
3489
3597
|
return eventName;
|
|
@@ -3652,7 +3760,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
3652
3760
|
return;
|
|
3653
3761
|
}
|
|
3654
3762
|
}
|
|
3655
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3763
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3656
3764
|
const eventName = getEventName(binding);
|
|
3657
3765
|
const valueGetter = getValueGetter(binding);
|
|
3658
3766
|
const isOccurrence = isOccurrenceProperty(binding);
|
|
@@ -3678,7 +3786,7 @@ function detachTwowayEventHandler(binding) {
|
|
|
3678
3786
|
return;
|
|
3679
3787
|
}
|
|
3680
3788
|
}
|
|
3681
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3789
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3682
3790
|
const eventName = getEventName(binding);
|
|
3683
3791
|
const valueGetter = getValueGetter(binding);
|
|
3684
3792
|
const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrenceProperty(binding));
|
|
@@ -4266,7 +4374,7 @@ function readOption(binding, key) {
|
|
|
4266
4374
|
continue;
|
|
4267
4375
|
const modifierKey = modifier.slice(0, separator).trim();
|
|
4268
4376
|
const value = modifier.slice(separator + 1).trim();
|
|
4269
|
-
if (modifierKey !==
|
|
4377
|
+
if (modifierKey !== MODIFIER_KEY_INIT && modifierKey !== MODIFIER_KEY_SYNC) {
|
|
4270
4378
|
raiseError(`Unknown binding modifier "${modifierKey}" in "${modifier}".`);
|
|
4271
4379
|
}
|
|
4272
4380
|
if (modifierKey !== key)
|
|
@@ -4313,8 +4421,8 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
4313
4421
|
}
|
|
4314
4422
|
return STATE_CALL_POLICY;
|
|
4315
4423
|
}
|
|
4316
|
-
const explicitAuthority = parseAuthority(readOption(binding,
|
|
4317
|
-
const syncOn = parseSyncOn(readOption(binding,
|
|
4424
|
+
const explicitAuthority = parseAuthority(readOption(binding, MODIFIER_KEY_INIT));
|
|
4425
|
+
const syncOn = parseSyncOn(readOption(binding, MODIFIER_KEY_SYNC));
|
|
4318
4426
|
if (binding.bindingType === "event") {
|
|
4319
4427
|
if (explicitAuthority !== null && explicitAuthority !== "none") {
|
|
4320
4428
|
raiseError("Event bindings only allow init=none.");
|
|
@@ -4326,7 +4434,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
4326
4434
|
// property authority 検証(未宣言なら raiseError)に掛けてはならない。値の初期同期を
|
|
4327
4435
|
// 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
|
|
4328
4436
|
// 初期 apply で配線される)。
|
|
4329
|
-
if (binding.propSegments[0] ===
|
|
4437
|
+
if (binding.propSegments[0] === COMMAND_NAMESPACE) {
|
|
4330
4438
|
return statePolicy("state", syncOn);
|
|
4331
4439
|
}
|
|
4332
4440
|
if (binding.bindingType !== "prop") {
|
|
@@ -5099,7 +5207,7 @@ class BindingSession {
|
|
|
5099
5207
|
record.eventAttached = true;
|
|
5100
5208
|
return;
|
|
5101
5209
|
}
|
|
5102
|
-
if (binding.propSegments[0] ===
|
|
5210
|
+
if (binding.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
5103
5211
|
this.attachAfterDefinition(record, () => {
|
|
5104
5212
|
if (attachEventTokenHandler(binding)) {
|
|
5105
5213
|
addRecordTeardown(record, () => detachEventTokenHandler(binding));
|
|
@@ -5125,7 +5233,7 @@ class BindingSession {
|
|
|
5125
5233
|
// isPossibleTwoWay の未定義 CE raiseError も踏まない)。
|
|
5126
5234
|
if (config.enableDirectionalInitialSync
|
|
5127
5235
|
&& isPossibleTwoWay(binding.node, binding.propName)
|
|
5128
|
-
&& binding.propModifiers.indexOf(
|
|
5236
|
+
&& binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
5129
5237
|
const removeObserver = addTwowayValueObserver(binding.node, binding.propName, (value) => {
|
|
5130
5238
|
if (!this.isAlive(record, record.generation))
|
|
5131
5239
|
return;
|
|
@@ -5544,7 +5652,8 @@ function applyChangeToCommand(binding, _context, newValue) {
|
|
|
5544
5652
|
raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
|
|
5545
5653
|
}
|
|
5546
5654
|
if (!bindable.declaredCommands.has(methodName)) {
|
|
5547
|
-
|
|
5655
|
+
// eventTokenHandler の property 検証と対双の did-you-mean(設計 §3)。
|
|
5656
|
+
raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.${didYouMean(methodName, bindable.declaredCommands.keys())}`);
|
|
5548
5657
|
}
|
|
5549
5658
|
// ここまで来たら旧解除して新 subscribe に切り替える。
|
|
5550
5659
|
if (existing) {
|
|
@@ -6204,7 +6313,7 @@ function compileRowPlan(fragmentInfo) {
|
|
|
6204
6313
|
// command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
|
|
6205
6314
|
// teardown / attach 分岐が要るため不適格
|
|
6206
6315
|
const namespace = template.propSegments[0];
|
|
6207
|
-
if (namespace ===
|
|
6316
|
+
if (namespace === COMMAND_NAMESPACE || namespace === EVENT_TOKEN_NAMESPACE) {
|
|
6208
6317
|
return null;
|
|
6209
6318
|
}
|
|
6210
6319
|
if (bindingType === "text") {
|
|
@@ -7298,12 +7407,15 @@ function scheduleDeferredApply(binding, tagName) {
|
|
|
7298
7407
|
}, reject);
|
|
7299
7408
|
}
|
|
7300
7409
|
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7410
|
+
// キーは define.ts の namespace 語彙定数(manifest.syntax.bindingTypes.propNamespaces と
|
|
7411
|
+
// 同一の正本)。集合の一致は __tests__/manifest.test.ts の drift テストが強制するため
|
|
7412
|
+
// export する(manifest エントリは DOM 非依存でこのファイルを import できない)。
|
|
7413
|
+
const applyChangeByFirstSegment = Object.freeze({
|
|
7414
|
+
[CLASS_NAMESPACE]: applyChangeToClass,
|
|
7415
|
+
[ATTR_NAMESPACE]: applyChangeToAttribute,
|
|
7416
|
+
[STYLE_NAMESPACE]: applyChangeToStyle,
|
|
7417
|
+
[COMMAND_NAMESPACE]: applyChangeToCommand,
|
|
7418
|
+
});
|
|
7307
7419
|
const applyChangeByBindingType = {
|
|
7308
7420
|
"text": applyChangeToText,
|
|
7309
7421
|
"for": applyChangeToFor,
|
|
@@ -8036,7 +8148,7 @@ async function buildBindings(root) {
|
|
|
8036
8148
|
}
|
|
8037
8149
|
}
|
|
8038
8150
|
|
|
8039
|
-
var version = "1.
|
|
8151
|
+
var version = "1.29.0";
|
|
8040
8152
|
var pkg = {
|
|
8041
8153
|
version: version};
|
|
8042
8154
|
|
|
@@ -9072,6 +9184,148 @@ function getUpdater() {
|
|
|
9072
9184
|
return updater;
|
|
9073
9185
|
}
|
|
9074
9186
|
|
|
9187
|
+
/**
|
|
9188
|
+
* devtools/declaredBindings.ts — 宣言レベルのバインディング列挙
|
|
9189
|
+
* (IDevtoolsSource.getDeclaredBindings の実装・protocol v1 追補)。
|
|
9190
|
+
*
|
|
9191
|
+
* devtools 側の DOM 再スキャン(declaredScan — 「正本 bindTextParser に非追随」と
|
|
9192
|
+
* 自己申告する簡易パーサ)を置き換える正本実装。ランタイム自身のパーサと
|
|
9193
|
+
* fragment レジストリで答えるため:
|
|
9194
|
+
* - パース結果は実行時解釈と**同一**(複製パーサのドリフトが構造的に消える)
|
|
9195
|
+
* - DOM から引き上げられた構造テンプレート内部(for/if の中身)も列挙できる
|
|
9196
|
+
* (DOM 再スキャンでは原理的に不可視だった領域)
|
|
9197
|
+
*
|
|
9198
|
+
* **戻り値は「宣言の集合」であり、インスタンスの multiset ではない**:
|
|
9199
|
+
* レンダリング済みページでは行クローンが `data-wcs` 属性とネストアンカー
|
|
9200
|
+
* (同一 UUID)を保持したまま live DOM に入るため、素朴な走査は宣言 1 件を
|
|
9201
|
+
* 行数分だけ重複列挙してしまう。ここでは宣言タプル(stateName / path / prop /
|
|
9202
|
+
* bindingType / フィルタ列)で dedupe し、宣言 1 件 = エントリ 1 件を保証する。
|
|
9203
|
+
* インスタンス粒度(どの行のどのノードか)は live の binding 台帳
|
|
9204
|
+
* (state:binding-added)の守備範囲で、こちらには持たせない。
|
|
9205
|
+
* 同一タプルの独立した静的宣言(同じバインディングを書いた別要素)も 1 件に
|
|
9206
|
+
* 畳まれる — node は最初に見つかったものを代表として持つ。
|
|
9207
|
+
*
|
|
9208
|
+
* fragment(構造テンプレート内部)は **rootNode 配下の live アンカーから到達可能な
|
|
9209
|
+
* UUID の推移閉包**として列挙する。ページ大域のレジストリを直接舐めないため、
|
|
9210
|
+
* 別 root の fragment や破棄済みビューの stale fragment は載らない。
|
|
9211
|
+
*
|
|
9212
|
+
* spread(`...:`)は attribute 起点では live Element の wcBindable から展開する
|
|
9213
|
+
* (設計 §5-1)。対象カスタム要素が未定義なら既存契約どおり spread のまま残す。
|
|
9214
|
+
*
|
|
9215
|
+
* **既知の盲点(時系列依存)**: 構造テンプレート外のテキストバインディングは、
|
|
9216
|
+
* binding start がコメントアンカーを空 Text に差し替えるため、**活性化後の DOM
|
|
9217
|
+
* から消えて列挙に現れない**(mustache は変換前も素の Text で不可視)。早期
|
|
9218
|
+
* アタッチ時は live 台帳(binding-added)が同領域を持つ。遅延アタッチでは
|
|
9219
|
+
* declared / live のどちらからも見えない — protocol §6 の既知非対称の一部。
|
|
9220
|
+
*
|
|
9221
|
+
* エラーパス以外もフック接続時にしか呼ばれない pull API であり、ホットパスには
|
|
9222
|
+
* 乗らない。壊れた宣言(不正 bindText)はエントリを捨てて先へ進む(検査は lint の
|
|
9223
|
+
* 責務 — ここは観測面)。
|
|
9224
|
+
*/
|
|
9225
|
+
// bindings/parseCommentNode.ts の EMBEDDED_REGEX と同形(keyword を自前で保持する
|
|
9226
|
+
// ために複製する — parseCommentNode は keyword を捨てて値しか返さないため、
|
|
9227
|
+
// 「未登録の構造アンカー(SSR pre-hydration ウィンドウ等)」と「埋め込みテキスト」
|
|
9228
|
+
// を区別できず、UUID をパスとして誤パースした bogus エントリが生まれる)。
|
|
9229
|
+
const COMMENT_PATTERN = /^\s*@@\s*(.*?)\s*:\s*(.+?)\s*$/;
|
|
9230
|
+
function declarationKey(info) {
|
|
9231
|
+
const filters = [...info.inFilters, ...info.outFilters]
|
|
9232
|
+
.map((f) => `${f.filterName}(${f.args.join(",")})`)
|
|
9233
|
+
.join("|");
|
|
9234
|
+
return [info.stateName, info.statePathName, info.propName, info.bindingType, filters].join("\u0000");
|
|
9235
|
+
}
|
|
9236
|
+
function toInfo(node, parsed, origin, raw) {
|
|
9237
|
+
return {
|
|
9238
|
+
node,
|
|
9239
|
+
propName: parsed.propName,
|
|
9240
|
+
statePathName: parsed.statePathName,
|
|
9241
|
+
stateName: parsed.stateName,
|
|
9242
|
+
bindingType: parsed.bindingType,
|
|
9243
|
+
inFilters: parsed.inFilters,
|
|
9244
|
+
outFilters: parsed.outFilters,
|
|
9245
|
+
origin,
|
|
9246
|
+
raw,
|
|
9247
|
+
};
|
|
9248
|
+
}
|
|
9249
|
+
/**
|
|
9250
|
+
* rootNode 配下の live DOM と、そこから到達可能な fragment から宣言集合を列挙する。
|
|
9251
|
+
* ヘッダの規定(宣言集合 / 到達閉包 / spread 展開 / テキストの盲点)を参照。
|
|
9252
|
+
*/
|
|
9253
|
+
function collectDeclaredBindings(rootNode) {
|
|
9254
|
+
const out = [];
|
|
9255
|
+
const seenKeys = new Set();
|
|
9256
|
+
const visitedFragments = new Set();
|
|
9257
|
+
const push = (info) => {
|
|
9258
|
+
const key = declarationKey(info);
|
|
9259
|
+
if (seenKeys.has(key))
|
|
9260
|
+
return;
|
|
9261
|
+
seenKeys.add(key);
|
|
9262
|
+
out.push(info);
|
|
9263
|
+
};
|
|
9264
|
+
/** fragment の内部宣言を emit し、ネストした構造アンカーへ再帰する(到達閉包)。 */
|
|
9265
|
+
const visitFragment = (uuid) => {
|
|
9266
|
+
if (visitedFragments.has(uuid))
|
|
9267
|
+
return;
|
|
9268
|
+
visitedFragments.add(uuid);
|
|
9269
|
+
const fragmentInfo = getFragmentInfoByUUID(uuid);
|
|
9270
|
+
if (fragmentInfo === null)
|
|
9271
|
+
return;
|
|
9272
|
+
for (const nodeInfo of fragmentInfo.nodeInfos) {
|
|
9273
|
+
for (const parsed of nodeInfo.parseBindTextResults) {
|
|
9274
|
+
push(toInfo(null, parsed, "fragment", ""));
|
|
9275
|
+
if (parsed.uuid != null) {
|
|
9276
|
+
visitFragment(parsed.uuid);
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9279
|
+
}
|
|
9280
|
+
};
|
|
9281
|
+
const doc = rootNode.ownerDocument ?? rootNode;
|
|
9282
|
+
const walker = doc.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
|
9283
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
9284
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
9285
|
+
const element = node;
|
|
9286
|
+
const bindText = element.getAttribute(config.bindAttributeName);
|
|
9287
|
+
if (bindText === null || bindText.length === 0)
|
|
9288
|
+
continue;
|
|
9289
|
+
try {
|
|
9290
|
+
// spread は live Element の wcBindable から展開(未定義要素は spread のまま = 既存契約)
|
|
9291
|
+
const results = expandSpread(element, parseBindTextsForElement(bindText));
|
|
9292
|
+
for (const parsed of results) {
|
|
9293
|
+
push(toInfo(element, parsed, "attribute", bindText));
|
|
9294
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
catch {
|
|
9297
|
+
// 不正な宣言は観測面では捨てる(検査は lint / ランタイム raiseError の責務)
|
|
9298
|
+
}
|
|
9299
|
+
continue;
|
|
9300
|
+
}
|
|
9301
|
+
// コメントアンカー: 構造ディレクティブ(UUID → fragment 登録済み)または
|
|
9302
|
+
// 埋め込みテキストバインディング。
|
|
9303
|
+
const match = COMMENT_PATTERN.exec(node.data.trim());
|
|
9304
|
+
if (match === null)
|
|
9305
|
+
continue;
|
|
9306
|
+
const keyword = match[1] || config.commentTextPrefix;
|
|
9307
|
+
const value = match[2];
|
|
9308
|
+
const fragmentInfo = getFragmentInfoByUUID(value);
|
|
9309
|
+
if (fragmentInfo !== null) {
|
|
9310
|
+
push(toInfo(node, fragmentInfo.parseBindTextResult, "comment", value));
|
|
9311
|
+
visitFragment(value);
|
|
9312
|
+
continue;
|
|
9313
|
+
}
|
|
9314
|
+
if (keyword !== config.commentTextPrefix) {
|
|
9315
|
+
// 構造アンカーなのに fragment 未登録(SSR の pre-hydration ウィンドウ等)、
|
|
9316
|
+
// または未知 keyword — UUID や不明値をパスとして誤パースしない。
|
|
9317
|
+
continue;
|
|
9318
|
+
}
|
|
9319
|
+
try {
|
|
9320
|
+
push(toInfo(node, parseBindTextForEmbeddedNode(value), "comment", value));
|
|
9321
|
+
}
|
|
9322
|
+
catch {
|
|
9323
|
+
// 同上
|
|
9324
|
+
}
|
|
9325
|
+
}
|
|
9326
|
+
return out;
|
|
9327
|
+
}
|
|
9328
|
+
|
|
9075
9329
|
/**
|
|
9076
9330
|
* devtools/types.ts
|
|
9077
9331
|
*
|
|
@@ -9213,6 +9467,9 @@ function createStateElementSummary(element) {
|
|
|
9213
9467
|
eventTokenNames: element.eventTokenNames,
|
|
9214
9468
|
staticDependency: element.staticDependency,
|
|
9215
9469
|
dynamicDependency: element.dynamicDependency,
|
|
9470
|
+
// protocol v1 追補(additive)— 配線カバレッジの宣言面(設計 §4)。
|
|
9471
|
+
// 旧 IStateElement 実装(optional)では undefined になりうるため null に畳む。
|
|
9472
|
+
watchPaths: element.watchPaths ?? null,
|
|
9216
9473
|
};
|
|
9217
9474
|
}
|
|
9218
9475
|
function requireStateElement(name, rootNode) {
|
|
@@ -9294,6 +9551,9 @@ function registerDevtoolsSource() {
|
|
|
9294
9551
|
}
|
|
9295
9552
|
});
|
|
9296
9553
|
},
|
|
9554
|
+
getDeclaredBindings(rootNode) {
|
|
9555
|
+
return collectDeclaredBindings(rootNode);
|
|
9556
|
+
},
|
|
9297
9557
|
_setSink: setSink,
|
|
9298
9558
|
};
|
|
9299
9559
|
registeredSource = source;
|
|
@@ -9636,7 +9896,7 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
|
|
|
9636
9896
|
}
|
|
9637
9897
|
for (const [name, handler] of Object.entries(declared)) {
|
|
9638
9898
|
if (!eventTokenNames.has(name)) {
|
|
9639
|
-
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens
|
|
9899
|
+
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.${didYouMean(name, eventTokenNames)}`);
|
|
9640
9900
|
}
|
|
9641
9901
|
if (typeof handler !== "function") {
|
|
9642
9902
|
raiseError(`${STATE_ON_NAME} entry "${name}" must be a function.`);
|
|
@@ -10674,41 +10934,44 @@ function processWatchDeclaration(stateElement, state) {
|
|
|
10674
10934
|
return null;
|
|
10675
10935
|
}
|
|
10676
10936
|
if (typeof declared !== "object" || declared === null) {
|
|
10677
|
-
|
|
10937
|
+
// lint 側の findNonObjectWatch が「断定できる形」を同 code で検出するため誘導を付ける。
|
|
10938
|
+
// (LINT_HINT を付けない残りは、lint が検出しない空キー・Object.prototype 継承名・
|
|
10939
|
+
// ワイルドカード深度超過の 3 shape。)
|
|
10940
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.${LINT_HINT}`);
|
|
10678
10941
|
}
|
|
10679
10942
|
const entries = new Map();
|
|
10680
10943
|
const paths = new Set();
|
|
10681
10944
|
let order = 0;
|
|
10682
10945
|
for (const [path, handler] of Object.entries(declared)) {
|
|
10683
10946
|
if (typeof handler !== "function") {
|
|
10684
|
-
raiseError(
|
|
10947
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must be a function.${LINT_HINT}`);
|
|
10685
10948
|
}
|
|
10686
10949
|
if (path.length === 0) {
|
|
10687
|
-
raiseError(
|
|
10950
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
|
|
10688
10951
|
}
|
|
10689
10952
|
if (path.startsWith("$")) {
|
|
10690
|
-
raiseError(
|
|
10953
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).${LINT_HINT}`);
|
|
10691
10954
|
}
|
|
10692
10955
|
// 越境 watch は不採用(設計 D8)。他 state のアドレスは発火対象にしないため、
|
|
10693
10956
|
// `@stateName` 付きのパスは受け取った時点で落とす(黙って発火しないより良い)。
|
|
10694
10957
|
if (path.includes(STATE_NAME_SEPARATOR)) {
|
|
10695
|
-
raiseError(
|
|
10958
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.${LINT_HINT}`);
|
|
10696
10959
|
}
|
|
10697
10960
|
// Object.prototype の継承名は `path in state` 系の判定を汚すため一律拒否する
|
|
10698
10961
|
// (processStreamsDeclaration と同じ防衛線)。
|
|
10699
10962
|
if (path in Object.prototype) {
|
|
10700
|
-
raiseError(
|
|
10963
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
10701
10964
|
}
|
|
10702
10965
|
const pathInfo = getPathInfo(path);
|
|
10703
10966
|
// 空セグメント("a..b" / 先頭・末尾の ".")は getPathInfo が黙って受理してしまうため、
|
|
10704
10967
|
// ここで落とす。放置すると解決不能なアドレスを依存グラフへ登録することになる。
|
|
10705
10968
|
for (const segment of pathInfo.segments) {
|
|
10706
10969
|
if (segment.length === 0) {
|
|
10707
|
-
raiseError(
|
|
10970
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" has an empty path segment.${LINT_HINT}`);
|
|
10708
10971
|
}
|
|
10709
10972
|
}
|
|
10710
10973
|
if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
|
|
10711
|
-
raiseError(
|
|
10974
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
|
|
10712
10975
|
}
|
|
10713
10976
|
entries.set(path, {
|
|
10714
10977
|
path,
|
|
@@ -11044,6 +11307,11 @@ function fireOne(hit) {
|
|
|
11044
11307
|
// handler 呼び出しより前に更新する
|
|
11045
11308
|
setComputedSnapshot(stateElement, absAddress, cur);
|
|
11046
11309
|
}
|
|
11310
|
+
// 正常発火の観測(設計書 §11 の予約イベント・配線カバレッジの実測面)。
|
|
11311
|
+
// 値は載せない。イベント生成は sink 接続時のみ(ゼロコスト契約)。
|
|
11312
|
+
if (devtoolsSink !== null) {
|
|
11313
|
+
devtoolsSink({ type: "state:watch-fired", stateName: stateElement.name, path: entry.path });
|
|
11314
|
+
}
|
|
11047
11315
|
entry.handler.call(state, cur, prev, ...indexes);
|
|
11048
11316
|
});
|
|
11049
11317
|
}
|
|
@@ -11171,6 +11439,20 @@ function getAllPropertyDescriptors(obj) {
|
|
|
11171
11439
|
* 双方向バインド・spread・initialSync の bindable 判定が**警告なしで**丸ごと死ぬ。
|
|
11172
11440
|
* 自前のファクトリが自前の reader に棄却される状態なので、生成前に落とす。
|
|
11173
11441
|
*/
|
|
11442
|
+
/**
|
|
11443
|
+
* did-you-mean の候補(エラーパス専用)。`$` 予約名と継承 `constructor` を除き、
|
|
11444
|
+
* `$bindables` には値プロパティだけ・`$commands` にはメソッドだけを提案する
|
|
11445
|
+
* (逆側を提案すると次は「is a method / is not a method」エラーに嵌まるため)。
|
|
11446
|
+
*/
|
|
11447
|
+
function dccCandidateNames(descriptors, kind) {
|
|
11448
|
+
return Object.keys(descriptors).filter((key) => {
|
|
11449
|
+
if (key.startsWith("$") || key === "constructor") {
|
|
11450
|
+
return false;
|
|
11451
|
+
}
|
|
11452
|
+
const isMethod = typeof descriptors[key].value === "function";
|
|
11453
|
+
return kind === "method" ? isMethod : !isMethod;
|
|
11454
|
+
});
|
|
11455
|
+
}
|
|
11174
11456
|
function readNameList(state, declarationName) {
|
|
11175
11457
|
const declared = state[declarationName];
|
|
11176
11458
|
if (typeof declared === "undefined") {
|
|
@@ -11223,7 +11505,7 @@ function processDccDeclarations(state) {
|
|
|
11223
11505
|
streamBackedBindables.push(name);
|
|
11224
11506
|
continue;
|
|
11225
11507
|
}
|
|
11226
|
-
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state
|
|
11508
|
+
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "value"))}`);
|
|
11227
11509
|
}
|
|
11228
11510
|
if (typeof descriptor.value === "function") {
|
|
11229
11511
|
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is a method. Declare it in ${STATE_COMMANDS_NAME} instead.`);
|
|
@@ -11232,7 +11514,7 @@ function processDccDeclarations(state) {
|
|
|
11232
11514
|
for (const name of commands) {
|
|
11233
11515
|
const descriptor = descriptors[name];
|
|
11234
11516
|
if (typeof descriptor === "undefined") {
|
|
11235
|
-
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state
|
|
11517
|
+
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "method"))}`);
|
|
11236
11518
|
}
|
|
11237
11519
|
if (typeof descriptor.value !== "function") {
|
|
11238
11520
|
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not a method. Declare it in ${STATE_BINDABLES_NAME} instead.`);
|
|
@@ -14722,6 +15004,27 @@ function getWcsManifest() {
|
|
|
14722
15004
|
},
|
|
14723
15005
|
// 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
|
|
14724
15006
|
structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
|
|
15007
|
+
modifiers: {
|
|
15008
|
+
flags: MODIFIER_FLAGS,
|
|
15009
|
+
keyValue: MODIFIER_KEYS,
|
|
15010
|
+
eventNamePrefix: EVENT_PROP_PREFIX,
|
|
15011
|
+
},
|
|
15012
|
+
indexParam: {
|
|
15013
|
+
prefix: INDEX_PARAM_PREFIX,
|
|
15014
|
+
maxDepth: MAX_WILDCARD_DEPTH,
|
|
15015
|
+
},
|
|
15016
|
+
bindingTypes: {
|
|
15017
|
+
elseKeyword: ELSE_KEYWORD,
|
|
15018
|
+
spread: SPREAD_PROP,
|
|
15019
|
+
eventPropertyPrefix: EVENT_PROP_PREFIX,
|
|
15020
|
+
propNamespaces: {
|
|
15021
|
+
eventToken: EVENT_TOKEN_NAMESPACE,
|
|
15022
|
+
command: COMMAND_NAMESPACE,
|
|
15023
|
+
class: CLASS_NAMESPACE,
|
|
15024
|
+
attr: ATTR_NAMESPACE,
|
|
15025
|
+
style: STYLE_NAMESPACE,
|
|
15026
|
+
},
|
|
15027
|
+
},
|
|
14725
15028
|
},
|
|
14726
15029
|
// 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
|
|
14727
15030
|
filters: Object.keys(outputBuiltinFilters),
|