@wcstack/state 1.32.0 → 1.33.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
@@ -1928,6 +1928,32 @@ function parsePropPart(propPart) {
1928
1928
  };
1929
1929
  }
1930
1930
 
1931
+ /**
1932
+ * 名前付き State(`<wcs-state name>` / `path@name`)の deprecation 通知。
1933
+ *
1934
+ * v2 では名前の次元そのものが消え、`<wcs-state mount="path">` と接頭辞付きパスに
1935
+ * 置き換わる(docs/state-mount-design.md D1 / D16)。1.x には `mount=` が無く、
1936
+ * warn を出しても利用者は動けないので、**既定では出さない**(`config.debug` 下だけ)。
1937
+ * 主経路は lint(`wcs/named-state-deprecated`)と README の告知。
1938
+ *
1939
+ * 出すときは種別 × 対象ごとに 1 回(起動のたびに同じ行が並ばないように)。
1940
+ */
1941
+ const reported$1 = new Set();
1942
+ function warnNamedStateDeprecated(kind, subject) {
1943
+ if (!config.debug) {
1944
+ return;
1945
+ }
1946
+ const key = `${kind}:${subject}`;
1947
+ if (reported$1.has(key)) {
1948
+ return;
1949
+ }
1950
+ reported$1.add(key);
1951
+ const hint = kind === 'attribute'
1952
+ ? `<wcs-state name="${subject}"> will be removed in v2. Mount the state onto the root tree with <wcs-state mount="${subject}"> and read it as "${subject}.<path>".`
1953
+ : `"${subject}" uses the "@name" state selector, which will be removed in v2. Read the mounted tree as "<name>.<path>" instead.`;
1954
+ console.warn(`[@wcstack/state] [wcs/named-state-deprecated] ${hint} See docs/state-mount-design.md §9.`);
1955
+ }
1956
+
1931
1957
  const cacheFilterInfos = new Map();
1932
1958
  // format: statePath@stateName|filter|filter
1933
1959
  // statePath-format: path.to.property (e.g., user.name.first, users.*.name, users.0.name, not include @)
@@ -1954,6 +1980,10 @@ function parseStatePart(statePart) {
1954
1980
  else {
1955
1981
  stateAndPath = statePart.trim();
1956
1982
  }
1983
+ if (stateAndPath.indexOf(STATE_NAME_SEPARATOR) !== -1) {
1984
+ // `path@name` は v2 で消える(docs/state-mount-design.md D16。既定では黙る)
1985
+ warnNamedStateDeprecated('path', stateAndPath);
1986
+ }
1957
1987
  const [statePathName, stateName = 'default'] = stateAndPath.split(STATE_NAME_SEPARATOR).map(trimFn);
1958
1988
  const pathInfo = getPathInfo(statePathName);
1959
1989
  return {
@@ -4019,11 +4049,12 @@ const outerMappingByElement = new WeakMap();
4019
4049
  const primaryMappingRuleSetByElement = new WeakMap();
4020
4050
  const primaryBindingByMappingRule = new WeakMap();
4021
4051
  function createMappingRuleByBinding(innerState, binding) {
4022
- const innerPathInfo = getPathInfo(binding.propSegments.slice(1).join(DELIMITER));
4052
+ const innerPath = binding.propSegments.slice(1).join(DELIMITER);
4053
+ const innerPathInfo = getPathInfo(innerPath);
4023
4054
  const innerAbsPathInfo = getAbsolutePathInfo(innerState, innerPathInfo);
4024
4055
  const outerAbsStateAddress = getAbsoluteStateAddressByBinding(binding);
4025
4056
  const outerAbsPathInfo = outerAbsStateAddress.absolutePathInfo;
4026
- return { innerAbsPathInfo, outerAbsPathInfo };
4057
+ return { innerAbsPathInfo, outerAbsPathInfo, isRoot: innerPath.length === 0 };
4027
4058
  }
4028
4059
  function buildPrimaryMappingRule(webComponent, stateName, bindings) {
4029
4060
  if (bindings.length === 0) {
@@ -4035,8 +4066,16 @@ function buildPrimaryMappingRule(webComponent, stateName, bindings) {
4035
4066
  }
4036
4067
  const innerMappingRule = new Map();
4037
4068
  const outerMappingRule = new Map();
4069
+ // 同じ内側パスを 2 つの規則が指す形(`state.x: b; state.x: c`、`state: a; state: b`)は
4070
+ // どちらが勝つか書き手に見えないので、導出時ではなく構築時に落とす(設計書 §4-7 / M6)。
4071
+ const seenInnerPaths = new Set();
4038
4072
  for (const binding of bindings) {
4039
4073
  const mappingRule = createMappingRuleByBinding(innerState, binding);
4074
+ const innerPath = mappingRule.innerAbsPathInfo.pathInfo.path;
4075
+ if (seenInnerPaths.has(innerPath)) {
4076
+ raiseError('Duplicate mapping rule for web component.');
4077
+ }
4078
+ seenInnerPaths.add(innerPath);
4040
4079
  let primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
4041
4080
  if (typeof primaryMappingRuleSet === 'undefined') {
4042
4081
  primaryMappingRuleSetByElement.set(webComponent, new Set([mappingRule]));
@@ -4084,11 +4123,14 @@ function resetDerivedMappingRules(webComponent) {
4084
4123
  outerMappingByElement.set(webComponent, outerMappingRule);
4085
4124
  }
4086
4125
  /**
4087
- * このコンポーネントに張られたプライマリ規則の**内側パス**を列挙する。
4126
+ * このコンポーネントに張られたプライマリ規則の**内側パス**を列挙する(ルート規則を除く)。
4088
4127
  *
4089
4128
  * 切断 → 再接続を跨いだ子(行 content の再利用で起きる)は、切断中に親で起きた変更の
4090
4129
  * 通知を受け取れていない。再接続時に「束ねているパスを読み直せ」と撃つための入力で、
4091
4130
  * 何が変わったかは分からないのでプライマリ規則の粒度で丸ごと読み直す(§1.9)。
4131
+ *
4132
+ * ルート規則は内側パスが空で `$postUpdate("")` に意味が無いため含めない。ルート規則の
4133
+ * 読み直しは子の登録済みパス全部(`getRootReloadPaths`)で行う。
4092
4134
  */
4093
4135
  function getPrimaryInnerPaths(webComponent) {
4094
4136
  const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
@@ -4097,13 +4139,37 @@ function getPrimaryInnerPaths(webComponent) {
4097
4139
  }
4098
4140
  const paths = [];
4099
4141
  for (const rule of primaryMappingRuleSet) {
4142
+ if (rule.isRoot) {
4143
+ continue;
4144
+ }
4100
4145
  paths.push(rule.innerAbsPathInfo.pathInfo.path);
4101
4146
  }
4102
4147
  return paths;
4103
4148
  }
4149
+ /** このコンポーネントのプライマリ規則の集合。無ければ null(plain)。 */
4150
+ function getPrimaryMappingRules(webComponent) {
4151
+ return primaryMappingRuleSetByElement.get(webComponent) ?? null;
4152
+ }
4153
+ /** ルート規則(`state: path` の丸ごとマウント)があるか。 */
4154
+ function hasRootMappingRule(webComponent) {
4155
+ const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
4156
+ if (typeof primaryMappingRuleSet === 'undefined') {
4157
+ return false;
4158
+ }
4159
+ for (const rule of primaryMappingRuleSet) {
4160
+ if (rule.isRoot) {
4161
+ return true;
4162
+ }
4163
+ }
4164
+ return false;
4165
+ }
4104
4166
  /**
4105
4167
  * 内側のパスを外側のパスへ翻訳する。規則が無ければプライマリ規則から導出する。
4106
4168
  *
4169
+ * 導出は**最長接頭辞一致**。ルート規則(接頭辞長 0)はあらゆる内側パスに一致するが、
4170
+ * 部分規則(`state.theme: theme`)がより長い接頭辞で一致すればそちらが勝つ
4171
+ * (`state: rows; state.theme: theme` の併用 — 設計書 §3-2)。
4172
+ *
4107
4173
  * `registerSubscriber` は導出に**副作用を持たせるか**の切り替え。既定(子の read /
4108
4174
  * write からの呼び出し)では導出した規則を台帳に memo し、対応するバインディングを
4109
4175
  * 親スコープの購読者として登録する。`false` を渡すと**参照専用**になり、台帳にも
@@ -4138,16 +4204,28 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo, registerSubscr
4138
4204
  return null;
4139
4205
  }
4140
4206
  let primaryMappingRule = null;
4207
+ let matchedPrefixLength = -1;
4141
4208
  for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
4209
+ if (currentPrimaryMappingRule.isRoot) {
4210
+ // ルート規則は接頭辞長 0 で常に一致。部分規則が無ければこれが採られる
4211
+ if (matchedPrefixLength < 0) {
4212
+ primaryMappingRule = currentPrimaryMappingRule;
4213
+ matchedPrefixLength = 0;
4214
+ }
4215
+ continue;
4216
+ }
4142
4217
  // innerPathInfoがprimaryMappingRuleのinnerPathInfoを包含しているか
4143
4218
  if (!innerAbsPathInfo.pathInfo.cumulativePathInfoSet.has(currentPrimaryMappingRule.innerAbsPathInfo.pathInfo)) {
4144
4219
  continue;
4145
4220
  }
4146
- if (currentPrimaryMappingRule.innerAbsPathInfo.pathInfo.segments.length === innerAbsPathInfo.pathInfo.segments.length) {
4221
+ const prefixLength = currentPrimaryMappingRule.innerAbsPathInfo.pathInfo.segments.length;
4222
+ if (prefixLength === innerAbsPathInfo.pathInfo.segments.length) {
4147
4223
  raiseError('Duplicate mapping rule for web component.');
4148
4224
  }
4149
- primaryMappingRule = currentPrimaryMappingRule;
4150
- break;
4225
+ if (prefixLength > matchedPrefixLength) {
4226
+ primaryMappingRule = currentPrimaryMappingRule;
4227
+ matchedPrefixLength = prefixLength;
4228
+ }
4151
4229
  }
4152
4230
  if (primaryMappingRule === null) {
4153
4231
  // マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
@@ -4160,7 +4238,7 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo, registerSubscr
4160
4238
  raiseError('Binding not found for primary mapping rule on web component.');
4161
4239
  }
4162
4240
  /* c8 ignore stop */
4163
- const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(primaryMappingRule.innerAbsPathInfo.pathInfo.segments.length);
4241
+ const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(matchedPrefixLength);
4164
4242
  const outerSegments = primaryMappingRule.outerAbsPathInfo.pathInfo.segments.concat(outerRemainingSegments);
4165
4243
  const outerPathInfo = getPathInfo(outerSegments.join(DELIMITER));
4166
4244
  const rootNode = webComponent.getRootNode();
@@ -5670,6 +5748,33 @@ function markWebComponentAsComplete(webComponent, stateProp) {
5670
5748
  function isWebComponentComplete(webComponent, stateProp) {
5671
5749
  return completedStatePropsByWebComponent.get(webComponent)?.has(stateProp) === true;
5672
5750
  }
5751
+ /**
5752
+ * `<wcs-state bind-component="<prop>">` が接続され、(webComponent, stateProp) を束ねると
5753
+ * **宣言した**台帳。完了(上)より前 — ホストの `whenDefined` / `waitInitializeBinding` を
5754
+ * 待つ前 — に記録する。
5755
+ *
5756
+ * 用途は丸ごとマウント `data-wcs="state: user"` の**完了前の初期適用の抑止**。完了前の
5757
+ * 1 セグメントバインディングは applyChangeToProperty が `element.state = userObject` と
5758
+ * 親のオブジェクトそのものをコンポーネントの state プロパティに書いてしまい、
5759
+ * bindWebComponent がそれを子 state の実体として取り込む — own data key が親のキー全部に
5760
+ * なり、R1 では全部が私有に化ける。宣言済みなら値を書かずに完了を待つ(子は完了後に
5761
+ * innerState 経由でライブに読む — docs/state-mount-design.md §3-2 / impl-plan P1-1)。
5762
+ *
5763
+ * 未宣言(子がまだ接続していない)の間は従来どおり書く。未 upgrade 要素への own property は
5764
+ * upgrade 時のクラスフィールド初期化で置き換わるので、実害は無い。
5765
+ */
5766
+ const declaredStatePropsByWebComponent = new WeakMap();
5767
+ function markWebComponentStatePropDeclared(webComponent, stateProp) {
5768
+ let declaredStateProps = declaredStatePropsByWebComponent.get(webComponent);
5769
+ if (!declaredStateProps) {
5770
+ declaredStateProps = new Set();
5771
+ declaredStatePropsByWebComponent.set(webComponent, declaredStateProps);
5772
+ }
5773
+ declaredStateProps.add(stateProp);
5774
+ }
5775
+ function isWebComponentStatePropDeclared(webComponent, stateProp) {
5776
+ return declaredStatePropsByWebComponent.get(webComponent)?.has(stateProp) === true;
5777
+ }
5673
5778
 
5674
5779
  function applyChangeToAttribute(binding, _context, newValue) {
5675
5780
  const element = binding.node;
@@ -7292,6 +7397,68 @@ function applyMirrorAttribute(element, attributeName, value) {
7292
7397
  element.setAttribute(attributeName, formatted);
7293
7398
  }
7294
7399
 
7400
+ /**
7401
+ * `bind-component` の**完了前**に親スコープの初期適用がコンポーネントの state プロパティへ
7402
+ * 行った書き込みの控え(docs/state-mount-design.md D19 / impl-plan P1-1・P1-10)。
7403
+ *
7404
+ * 完了前の親→子の適用は `applyChangeToProperty` が素のプロパティに値を積む
7405
+ * (webComponent/completeWebComponent.ts)。丸ごとマウントと R1 が入って、この積みが
7406
+ * 2 つの取り違えを生むようになった。
7407
+ *
7408
+ * 1. **丸ごと(1 セグメント・`state: user`)**: `element.state = userObject` と親の
7409
+ * オブジェクトそのもので state プロパティを**置き換える**。子がまだ宣言していない
7410
+ * タイミング(happy-dom の template clone は upgrade 済みで、挿入前に適用が走る)では
7411
+ * 宣言台帳のガードが効かず、作者の state オブジェクト(getter / 私有キー)が失われる。
7412
+ * → 置き換え前のオブジェクトを控え、子の初期化時に戻す(`takeOverwrittenObject`)。
7413
+ * 2. **部分(2 セグメント・`state.theme: theme`)**: `element.state.theme = themeObject` と
7414
+ * 作者のオブジェクトに**キーを注入する**。R1 の判定はこれを own data key と区別できない。
7415
+ * → 注入したキーを控え、衝突の報告(ownKeyShadow)から外す(`getInjectedKeys`)。
7416
+ * 私有判定そのものは innerState が「部分規則が覆うキー」を静的に除くので、ここには依らない。
7417
+ *
7418
+ * どちらもカスタム要素・オブジェクト値のときだけ記録するので、通常のプロパティ書き込み
7419
+ * (textContent / value / checked …)のホットパスには typeof 判定 1 つしか載らない。
7420
+ */
7421
+ const overwrittenObjectByElement = new WeakMap();
7422
+ const injectedKeysByElement = new WeakMap();
7423
+ /** 1 セグメント書き込みで置き換えられる直前のオブジェクトを控える。最初の 1 回だけ(作者のもの)。 */
7424
+ function rememberOverwrittenObject(element, prop, previous) {
7425
+ let byProp = overwrittenObjectByElement.get(element);
7426
+ if (!byProp) {
7427
+ byProp = new Map();
7428
+ overwrittenObjectByElement.set(element, byProp);
7429
+ }
7430
+ if (!byProp.has(prop)) {
7431
+ byProp.set(prop, previous);
7432
+ }
7433
+ }
7434
+ /** 控えを取り出して消す。無ければ undefined。 */
7435
+ function takeOverwrittenObject(element, prop) {
7436
+ const byProp = overwrittenObjectByElement.get(element);
7437
+ if (!byProp) {
7438
+ return undefined;
7439
+ }
7440
+ const previous = byProp.get(prop);
7441
+ byProp.delete(prop);
7442
+ return previous;
7443
+ }
7444
+ /** 2 セグメント書き込み(`state.theme`)が作者のオブジェクトに無かったキーを作ったことを控える。 */
7445
+ function recordInjectedKey(element, prop, key) {
7446
+ let byProp = injectedKeysByElement.get(element);
7447
+ if (!byProp) {
7448
+ byProp = new Map();
7449
+ injectedKeysByElement.set(element, byProp);
7450
+ }
7451
+ let keys = byProp.get(prop);
7452
+ if (!keys) {
7453
+ keys = new Set();
7454
+ byProp.set(prop, keys);
7455
+ }
7456
+ keys.add(key);
7457
+ }
7458
+ function getInjectedKeys(element, prop) {
7459
+ return injectedKeysByElement.get(element)?.get(prop);
7460
+ }
7461
+
7295
7462
  /**
7296
7463
  * SSR 時に HTML 属性で表現できないプロパティバインディングを蓄積するストア。
7297
7464
  * ハイドレーション時にクライアント側で復元する。
@@ -7388,7 +7555,17 @@ function applyChangeToProperty(binding, _context, newValue) {
7388
7555
  const propSegments = binding.propSegments;
7389
7556
  if (propSegments.length === 1) {
7390
7557
  const firstSegment = propSegments[0];
7391
- if (element[firstSegment] !== newValue) {
7558
+ const current = element[firstSegment];
7559
+ if (current !== newValue) {
7560
+ // 完了前の丸ごとマウント(`state: user`)は、作者の state オブジェクトを親の
7561
+ // オブジェクトで置き換えてしまう。あとで戻せるように置き換え前を控える
7562
+ // (webComponent/preCompletionWrites.ts)。オブジェクト → オブジェクトの書き込みで
7563
+ // 相手がカスタム要素のときだけ台帳に触る(通常の書き込みは typeof 判定で抜ける)。
7564
+ if (current !== null && typeof current === 'object'
7565
+ && newValue !== null && typeof newValue === 'object'
7566
+ && getCustomElement(element) !== null) {
7567
+ rememberOverwrittenObject(element, firstSegment, current);
7568
+ }
7392
7569
  const performWrite = () => {
7393
7570
  let propertyWriteSucceeded = false;
7394
7571
  try {
@@ -7490,7 +7667,8 @@ function applyChangeToProperty(binding, _context, newValue) {
7490
7667
  }
7491
7668
  subObject = subObject[segment];
7492
7669
  }
7493
- const oldValue = subObject[propSegments[propSegments.length - 1]];
7670
+ const lastSegment = propSegments[propSegments.length - 1];
7671
+ const oldValue = subObject[lastSegment];
7494
7672
  if (oldValue !== newValue) {
7495
7673
  if (Object.isFrozen(subObject)) {
7496
7674
  if (config.debug) {
@@ -7503,8 +7681,15 @@ function applyChangeToProperty(binding, _context, newValue) {
7503
7681
  }
7504
7682
  return;
7505
7683
  }
7684
+ // 完了前の部分マウント(`state.theme: theme`)が、作者の state オブジェクトに無かった
7685
+ // キーを作る(積み)ことを控える。R1 の衝突報告はこのキーを作者のものとして扱わない
7686
+ // (webComponent/preCompletionWrites.ts)
7687
+ if (propSegments.length === 2 && typeof subObject === 'object' && subObject !== null
7688
+ && !(lastSegment in subObject) && getCustomElement(element) !== null) {
7689
+ recordInjectedKey(element, firstSegment, lastSegment);
7690
+ }
7506
7691
  try {
7507
- subObject[propSegments[propSegments.length - 1]] = newValue;
7692
+ subObject[lastSegment] = newValue;
7508
7693
  }
7509
7694
  catch (error) {
7510
7695
  if (config.debug) {
@@ -7562,6 +7747,34 @@ function applyChangeToText(binding, _context, newValue) {
7562
7747
  }
7563
7748
  }
7564
7749
 
7750
+ /**
7751
+ * ルート規則(`data-wcs="state: path"` の丸ごとマウント)で、親がマウント先を
7752
+ * 丸ごと差し替えた/子が切断 → 再接続した、というときに子へ「読み直せ」と撃つパスの集合。
7753
+ *
7754
+ * 部分規則ならプライマリの内側パス(`state.items: rows` なら `items`)を撃てば依存 walk が
7755
+ * 配下へ展開するが、ルート規則は内側パスが空で `$postUpdate("")` に意味が無い。
7756
+ * 代わりに子の登録済みパス(`boundPaths`)の**先頭セグメント**を撃つ — `tags.*.name` は
7757
+ * `tags` から静的依存で展開されるので、先頭だけで配下を覆える。
7758
+ *
7759
+ * `$` 名前空間(`$1` など)は state に実体を持たないので除く。私有キー(R1)への通知は
7760
+ * 値が変わっていないので再描画が同値で終わるだけ(無害)。
7761
+ */
7762
+ function getRootReloadPaths(innerStateElement) {
7763
+ const boundPaths = innerStateElement.boundPaths;
7764
+ if (typeof boundPaths === 'undefined') {
7765
+ return [];
7766
+ }
7767
+ const roots = new Set();
7768
+ for (const path of boundPaths) {
7769
+ if (path[0] === '$') {
7770
+ continue;
7771
+ }
7772
+ const dot = path.indexOf(DELIMITER);
7773
+ roots.add(dot === -1 ? path : path.slice(0, dot));
7774
+ }
7775
+ return [...roots];
7776
+ }
7777
+
7565
7778
  /**
7566
7779
  * 親 state → `bind-component` 済みコンポーネントの再読込通知(内部チャネル)。
7567
7780
  *
@@ -7580,13 +7793,14 @@ function applyChangeToText(binding, _context, newValue) {
7580
7793
  * (apply/applyChange.ts)、`bindWebComponent` は完了済み = state element は登録済み。
7581
7794
  * ただし**登録済みと使用可能は別**で、切断済みの state element が台帳に残っている
7582
7795
  * 窓がある(§1.9)。下の使用可能判定を参照。
7796
+ *
7797
+ * 残余パスが空(`data-wcs="state: user"` — ルート規則の丸ごとマウント)は、親が
7798
+ * マウント先を丸ごと差し替えたという通知。何が変わったかは分からないので、子の
7799
+ * 登録済みパス全部を読み直す(docs/state-mount-design.md §3-2 / impl-plan P1-2)。
7583
7800
  */
7584
7801
  function applyChangeToWebComponent(binding, _context, _newValue) {
7585
7802
  const element = binding.node;
7586
7803
  const propSegments = binding.propSegments;
7587
- if (propSegments.length <= 1) {
7588
- raiseError(`Invalid propSegments for web component binding: ${propSegments.join(DELIMITER)}`);
7589
- }
7590
7804
  const [firstSegment, ...restSegments] = propSegments;
7591
7805
  const innerStateElement = getStateElementByWebComponent(element, firstSegment);
7592
7806
  if (innerStateElement === null) {
@@ -7611,6 +7825,18 @@ function applyChangeToWebComponent(binding, _context, _newValue) {
7611
7825
  }
7612
7826
  return;
7613
7827
  }
7828
+ if (restSegments.length === 0) {
7829
+ const paths = getRootReloadPaths(innerStateElement);
7830
+ if (paths.length === 0) {
7831
+ return;
7832
+ }
7833
+ innerStateElement.createState("readonly", (state) => {
7834
+ for (const path of paths) {
7835
+ state.$postUpdate(path);
7836
+ }
7837
+ });
7838
+ return;
7839
+ }
7614
7840
  innerStateElement.createState("readonly", (state) => {
7615
7841
  state.$postUpdate(restSegments.join(DELIMITER));
7616
7842
  });
@@ -7997,21 +8223,36 @@ const deferredSelectBindingByBinding = new WeakMap();
7997
8223
  // registry 照会を省略できる。scoped registry を導入する場合はこの不可逆前提を再検討。
7998
8224
  const definedApplyVerifiedByBinding = new WeakMap();
7999
8225
  /**
8000
- * このバインディングを「値を運ばない親→子の再読込通知」(applyChangeToWebComponent)へ
8001
- * 回してよいか。
8226
+ * 丸ごとマウント(`state: user`)の完了前の初期適用は書かない。子が完了すれば
8227
+ * innerState 経由でライブに読むので、ここで親のオブジェクトを書く意味は無い
8228
+ * (書くと害がある — webComponent/completeWebComponent.ts の宣言台帳を参照)。
8229
+ */
8230
+ function skipPendingRootMount() { }
8231
+ /**
8232
+ * カスタム要素へのプロパティバインディングの適用関数を決める。
8233
+ *
8234
+ * - 完了済み(bindWebComponent が公開プロパティを差し替え終えた)→ 値を運ばない
8235
+ * 再読込通知(applyChangeToWebComponent)。1 セグメント(`state: user`)も含む —
8236
+ * 残余が空なら「子の登録済みパス全部を読み直せ」の意味(ルート規則。
8237
+ * docs/state-mount-design.md §3-2 / impl-plan P1-2)
8238
+ * - 未完了だが `<wcs-state bind-component>` が宣言済みで、かつ 1 セグメント → 今回は
8239
+ * 書かない(skipPendingRootMount)
8240
+ * - それ以外 → 素のプロパティ書き込み(`state.name: x` の完了前の積みも含む)
8002
8241
  *
8003
- * 長さ 1 propSegments を除くのが要点。`data-wcs="state: user"` のように
8004
- * bind-componentstateProp をそのままプロパティ名に書いた形は、完了台帳のキーが
8005
- * stateProp 名になった以上ゲートを通ってしまうが、applyChangeToWebComponent は
8006
- * 「先頭セグメント=束ね先の state 要素、残り=子側のパス」を前提にしており
8007
- * 残余が空だと raiseError する。updater の drain は例外を捕まえないので、
8008
- * 誤設定タグ 1 つが同じバッチの無関係な更新まで巻き添えにしてしまう。
8009
- * ここで弾いておけば従来どおり applyChangeToProperty に落ち、挙動は変わらない
8010
- * (getter だけの公開プロパティへの代入が握り潰される = 無言の no-op)。
8242
+ * 以前は 1 セグメントを通知チャネルから除いていた(残余が空だと applyChangeToWebComponent
8243
+ * raiseError し、updater drain が捕まえないので同じバッチの無関係な更新まで
8244
+ * 巻き添えにした)。残余空がルート規則の意味を持った今、その除外は要らない。
8011
8245
  */
8012
- function isWebComponentCompleteForBinding(binding) {
8013
- return binding.propSegments.length > 1
8014
- && isWebComponentComplete(binding.replaceNode, binding.propSegments[0]);
8246
+ function resolveCustomElementApply(binding) {
8247
+ const element = binding.replaceNode;
8248
+ const stateProp = binding.propSegments[0];
8249
+ if (isWebComponentComplete(element, stateProp)) {
8250
+ return applyChangeToWebComponent;
8251
+ }
8252
+ if (binding.propSegments.length === 1 && isWebComponentStatePropDeclared(element, stateProp)) {
8253
+ return skipPendingRootMount;
8254
+ }
8255
+ return applyChangeToProperty;
8015
8256
  }
8016
8257
  function _applyChange(binding, context) {
8017
8258
  const value = getValue(context.state, binding);
@@ -8026,13 +8267,10 @@ function _applyChange(binding, context) {
8026
8267
  return;
8027
8268
  }
8028
8269
  if (fnByBinding.has(binding)) {
8029
- if (isWebComponentCompleteForBinding(binding)) {
8030
- fn = applyChangeToWebComponent;
8270
+ fn = resolveCustomElementApply(binding);
8271
+ if (fn === applyChangeToWebComponent) {
8031
8272
  fnByBinding.set(binding, fn); // 確定したのでキャッシュ
8032
8273
  }
8033
- else {
8034
- fn = applyChangeToProperty;
8035
- }
8036
8274
  fn(binding, context, filteredValue);
8037
8275
  return;
8038
8276
  }
@@ -8044,13 +8282,10 @@ function _applyChange(binding, context) {
8044
8282
  if (typeof fn === 'undefined') {
8045
8283
  const customTag = getCustomElement(binding.replaceNode);
8046
8284
  if (customTag) {
8047
- if (isWebComponentCompleteForBinding(binding)) {
8048
- fn = applyChangeToWebComponent;
8285
+ fn = resolveCustomElementApply(binding);
8286
+ if (fn === applyChangeToWebComponent) {
8049
8287
  fnByBinding.set(binding, fn); // 確定したのでキャッシュ
8050
8288
  }
8051
- else {
8052
- fn = applyChangeToProperty;
8053
- }
8054
8289
  }
8055
8290
  else {
8056
8291
  fn = applyChangeToProperty;
@@ -8753,7 +8988,7 @@ async function buildBindings(root) {
8753
8988
  }
8754
8989
  }
8755
8990
 
8756
- var version = "1.32.0";
8991
+ var version = "1.33.0";
8757
8992
  var pkg = {
8758
8993
  version: version};
8759
8994
 
@@ -10533,9 +10768,19 @@ function describeImportFailure(name, error, cspBlocked) {
10533
10768
  return `Failed to evaluate the inline <script> of state "${name}": ${detail}. ` +
10534
10769
  `If this page sets a Content-Security-Policy, see ${CSP_GUIDE}`;
10535
10770
  }
10771
+ /**
10772
+ * ロードごとに増える通し番号。`data:` URL フォールバック(createObjectURL の無い
10773
+ * テスト / SSR 環境)では URL がスクリプト本文そのものなので、同じ本文を 2 度
10774
+ * 読み込むと ESM ローダーのキャッシュに当たり **同じモジュール = 同じ default export
10775
+ * オブジェクト** が返る — 2 つ目の `<wcs-state>` が 1 つ目の state を共有してしまう
10776
+ * (テストでは前のテストの書き込みが次のテストに漏れ、SSR では同一テンプレートの
10777
+ * 再描画で state が共有される)。blob: URL は生成のたびに一意なのでブラウザでは
10778
+ * 起きない。sourceURL コメントに番号を混ぜて本文を毎回変え、両経路を揃える。
10779
+ */
10780
+ let loadSequence = 0;
10536
10781
  async function loadFromInnerScript(script, name) {
10537
10782
  let scriptModule = null;
10538
- const uniq_comment = `\n//# sourceURL=${name}\n`;
10783
+ const uniq_comment = `\n//# sourceURL=${name}#${++loadSequence}\n`;
10539
10784
  // import() が失敗した理由が CSP かどうかを判別するために、評価の間だけ違反を購読する。
10540
10785
  let cspBlocked = false;
10541
10786
  const onViolation = (event) => {
@@ -15010,9 +15255,58 @@ function meltFrozenObject(frozenObj) {
15010
15255
  class InnerStateProxyHandler {
15011
15256
  _webComponent;
15012
15257
  _innerStateElement;
15258
+ /**
15259
+ * ルート規則(`state: path` の丸ごとマウント)の下にいるか。プライマリ規則は
15260
+ * bindWebComponent が createInnerState より先に組むので、構築時に確定する。
15261
+ */
15262
+ _rootMounted;
15263
+ /**
15264
+ * 部分規則(`state.theme: theme`)が覆う内側の先頭セグメント。ルート規則と併用された
15265
+ * とき、これらのキーは own data key があってもマッピング(1.x の既存挙動)に落とす。
15266
+ * 完了前の親の初期適用が `element.state.theme = obj` とキーを注入する(積み)ので、
15267
+ * own key の有無では作者の意図を判定できない(webComponent/preCompletionWrites.ts)。
15268
+ */
15269
+ _partialFirstSegments;
15013
15270
  constructor(webComponent, stateName) {
15014
15271
  this._webComponent = webComponent;
15015
15272
  this._innerStateElement = getStateElementByWebComponent(webComponent, stateName) ?? raiseError('State element not found for web component.');
15273
+ this._rootMounted = hasRootMappingRule(webComponent);
15274
+ this._partialFirstSegments = null;
15275
+ if (this._rootMounted) {
15276
+ const rules = getPrimaryMappingRules(webComponent);
15277
+ for (const rule of rules ?? []) {
15278
+ if (!rule.isRoot) {
15279
+ (this._partialFirstSegments ??= new Set()).add(rule.innerAbsPathInfo.pathInfo.segments[0]);
15280
+ }
15281
+ }
15282
+ }
15283
+ }
15284
+ /**
15285
+ * R1(docs/state-mount-design.md D4 / D19): ルートマウント下では、コンポーネントが
15286
+ * 自分で書いた data key は**私有**で、マウント先のツリーを隠す。v1 の解決順
15287
+ * (getter → マッピング → ローカル)のままルート規則を載せると、ルート規則は全キーに
15288
+ * 一致するので own key が全部ツリーに隠され、2.0 と逆の意味論で出荷することになる。
15289
+ *
15290
+ * 判定は**先頭セグメント**で行う。getByAddress は `"draft.title" in target` → 偽 →
15291
+ * 親アドレス `draft` の読み、と降りてくるので、先頭が私有ならその下は素のオブジェクト
15292
+ * 走査になる(plain な state と同じ)。getter / setter は規則 1(chroot 評価)なので除く。
15293
+ *
15294
+ * 部分マウントだけのコンポーネントには適用しない(1.x の既存挙動=マッピングが勝つ、を
15295
+ * 維持する。2.0 での反転は bindWebComponent が warn で予告する — D19)。
15296
+ */
15297
+ _isPrivateKey(target, prop) {
15298
+ if (!this._rootMounted) {
15299
+ return false;
15300
+ }
15301
+ const dot = prop.indexOf(DELIMITER);
15302
+ const first = dot === -1 ? prop : prop.slice(0, dot);
15303
+ if (!Object.prototype.hasOwnProperty.call(target, first)) {
15304
+ return false;
15305
+ }
15306
+ if (this._partialFirstSegments !== null && this._partialFirstSegments.has(first)) {
15307
+ return false;
15308
+ }
15309
+ return !this._innerStateElement.getterPaths.has(first) && !this._innerStateElement.setterPaths.has(first);
15016
15310
  }
15017
15311
  /**
15018
15312
  * 親スコープで読み書きするときのループ文脈を決める。候補は 2 つある。
@@ -15065,6 +15359,10 @@ class InnerStateProxyHandler {
15065
15359
  if (this._innerStateElement.getterPaths.has(prop) && prop in target) {
15066
15360
  return Reflect.get(target, prop, receiver);
15067
15361
  }
15362
+ // 1'. R1: ルートマウント下の own data key → 私有(マッピングより先)
15363
+ if (this._isPrivateKey(target, prop)) {
15364
+ return Reflect.get(target, prop, receiver);
15365
+ }
15068
15366
  // 2 & 3. マッピング完全一致 / サブパス → 親の状態
15069
15367
  const innerPathInfo = getPathInfo(prop);
15070
15368
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
@@ -15096,6 +15394,10 @@ class InnerStateProxyHandler {
15096
15394
  if (this._innerStateElement.setterPaths.has(prop) && prop in target) {
15097
15395
  return Reflect.set(target, prop, value, receiver);
15098
15396
  }
15397
+ // 1'. R1: ルートマウント下の own data key → 私有に書く
15398
+ if (this._isPrivateKey(target, prop)) {
15399
+ return Reflect.set(target, prop, value, receiver);
15400
+ }
15099
15401
  // 2 & 3. マッピング完全一致 / サブパス → 親に書く
15100
15402
  const innerPathInfo = getPathInfo(prop);
15101
15403
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
@@ -15129,6 +15431,11 @@ class InnerStateProxyHandler {
15129
15431
  if ((this._innerStateElement.getterPaths.has(prop) || this._innerStateElement.setterPaths.has(prop)) && prop in target) {
15130
15432
  return true;
15131
15433
  }
15434
+ // 1'. R1: 先頭が私有キーなら素のオブジェクトの答え(完全一致だけ真。`draft.title` は
15435
+ // 偽を返して getByAddress に親アドレスから降りてもらう)
15436
+ if (this._isPrivateKey(target, prop)) {
15437
+ return prop in target;
15438
+ }
15132
15439
  // 2 & 3. マッピング
15133
15440
  const innerPathInfo = getPathInfo(prop);
15134
15441
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
@@ -15220,6 +15527,116 @@ function createOuterState(webComponent, stateName) {
15220
15527
  return new Proxy({}, handler);
15221
15528
  }
15222
15529
 
15530
+ /**
15531
+ * コンポーネントの own data key とマウントの衝突を、バインド確立時に 1 回だけ報告する
15532
+ * (docs/state-mount-design.md D4 / D19、impl-plan P1-10 / P1-11)。
15533
+ *
15534
+ * 2 つの形がある。
15535
+ *
15536
+ * - **ルートマウント**(`state: user`): R1 では own data key は私有で、マウント先の
15537
+ * 同名キー(`user.name`)を**隠す**。書き手が「既定値」のつもりで置いたキーがツリーを
15538
+ * 読まなくなるので、マウント先の値がオブジェクトで同名キーを持つときに報告する。
15539
+ * - **部分マウント**(`state.message: x` + `state = { message: "" }`): 1.x では
15540
+ * マッピングが勝つ(既存挙動・不変)が、v2 では R1 で own key が私有になり逆転する。
15541
+ * 反転を 1.x の時点で予告する。
15542
+ *
15543
+ * どちらも「既定値を消す(ツリーを読む)か、名前を変える(私有のまま)」で直る。
15544
+ * 報告はタグ名 × プロパティ × キーで 1 回(リストの行ごとに並ばないように)。
15545
+ * ホットパス外(bindWebComponent の中・要素につき 1 回)。
15546
+ */
15547
+ const reported = new Set();
15548
+ function ownDataKeys(state, injected) {
15549
+ const keys = [];
15550
+ for (const key of Object.keys(state)) {
15551
+ if (key.startsWith('$')) {
15552
+ continue;
15553
+ }
15554
+ // 完了前の親の初期適用(`state.theme: theme` の積み)が作ったキーは作者のものではない
15555
+ if (typeof injected !== 'undefined' && injected.has(key)) {
15556
+ continue;
15557
+ }
15558
+ const descriptor = Object.getOwnPropertyDescriptor(state, key);
15559
+ if (typeof descriptor.get === 'function' || typeof descriptor.set === 'function') {
15560
+ continue;
15561
+ }
15562
+ if (typeof descriptor.value === 'function') {
15563
+ continue;
15564
+ }
15565
+ keys.push(key);
15566
+ }
15567
+ return keys;
15568
+ }
15569
+ /**
15570
+ * ルート規則のマウント先の現在値を読む。ワイルドカードを含むマウント先(`users.*`)は
15571
+ * ホスト要素のループ文脈で解決する。文脈が無ければ判定できないので undefined。
15572
+ */
15573
+ function readMountTarget(component, rule) {
15574
+ const outer = rule.outerAbsPathInfo;
15575
+ const loopContext = getLoopContextByNode(component);
15576
+ if (outer.pathInfo.wildcardCount > 0 && loopContext === null) {
15577
+ return undefined;
15578
+ }
15579
+ let value = undefined;
15580
+ outer.stateElement.createState("readonly", (state) => {
15581
+ state[setLoopContextSymbol](loopContext, () => {
15582
+ value = state[outer.pathInfo.path];
15583
+ });
15584
+ });
15585
+ return value;
15586
+ }
15587
+ function report(key, message) {
15588
+ if (reported.has(key)) {
15589
+ return;
15590
+ }
15591
+ reported.add(key);
15592
+ console.warn(`[@wcstack/state] [wcs/mount-own-key-shadow] ${message} See docs/state-mount-design.md §4-3.`);
15593
+ }
15594
+ function warnOwnKeyShadows(component, stateProp, state) {
15595
+ const rules = getPrimaryMappingRules(component);
15596
+ if (rules === null) {
15597
+ return;
15598
+ }
15599
+ const keys = ownDataKeys(state, getInjectedKeys(component, stateProp));
15600
+ if (keys.length === 0) {
15601
+ return;
15602
+ }
15603
+ const tag = component.tagName.toLowerCase();
15604
+ let rootRule = null;
15605
+ const partialRuleByKey = new Map();
15606
+ for (const rule of rules) {
15607
+ if (rule.isRoot) {
15608
+ rootRule = rule;
15609
+ }
15610
+ else if (rule.innerAbsPathInfo.pathInfo.segments.length === 1) {
15611
+ partialRuleByKey.set(rule.innerAbsPathInfo.pathInfo.path, rule);
15612
+ }
15613
+ }
15614
+ let mountTarget = undefined;
15615
+ let mountTargetRead = false;
15616
+ for (const key of keys) {
15617
+ const partial = partialRuleByKey.get(key);
15618
+ if (typeof partial !== 'undefined') {
15619
+ const outerPath = partial.outerAbsPathInfo.pathInfo.path;
15620
+ report(`${tag}|${stateProp}|${key}|partial`, `<${tag}>.${stateProp}.${key} is an own key of the component and is also mapped from the host ("${stateProp}.${key}: ${outerPath}"). ` +
15621
+ `Today the host value wins; in v2 the own key becomes private and hides the host value. Remove the default, or drop the mapping.`);
15622
+ continue;
15623
+ }
15624
+ if (rootRule === null) {
15625
+ continue;
15626
+ }
15627
+ if (!mountTargetRead) {
15628
+ mountTarget = readMountTarget(component, rootRule);
15629
+ mountTargetRead = true;
15630
+ }
15631
+ if (typeof mountTarget !== 'object' || mountTarget === null || !(key in mountTarget)) {
15632
+ continue;
15633
+ }
15634
+ const outerPath = rootRule.outerAbsPathInfo.pathInfo.path;
15635
+ report(`${tag}|${stateProp}|${key}|root`, `<${tag}>.${stateProp}.${key} is private and hides the mounted tree key "${outerPath}.${key}" (${stateProp}: ${outerPath}). ` +
15636
+ `Remove the default to read the tree, or rename it to keep it private.`);
15637
+ }
15638
+ }
15639
+
15223
15640
  const getOuter = (outerState) => () => outerState;
15224
15641
  function bindWebComponent(innerStateElement, component, stateProp, state) {
15225
15642
  setStateElementByWebComponent(component, stateProp, innerStateElement);
@@ -15236,6 +15653,9 @@ function bindWebComponent(innerStateElement, component, stateProp, state) {
15236
15653
  // innerState proxy、plain なら melt 済みのローカル state。
15237
15654
  if (bindings.length > 0) {
15238
15655
  buildPrimaryMappingRule(component, stateProp, bindings);
15656
+ // own data key とマウントの衝突を 1 回だけ報告する(R1 の私有キーがツリーを隠す形と、
15657
+ // 部分マウントで v2 に反転する形 — docs/state-mount-design.md D19)。
15658
+ warnOwnKeyShadows(component, stateProp, state);
15239
15659
  // 値の正本が親スコープにあることを state 要素に記録する。越境アドレスの受け渡しと
15240
15660
  // リストパスの外向き伝播はこのフラグでのみ有効になる(§1.8)。
15241
15661
  innerStateElement.markComponentStateMapped?.();
@@ -15266,6 +15686,27 @@ function bindWebComponent(innerStateElement, component, stateProp, state) {
15266
15686
  }
15267
15687
  }
15268
15688
 
15689
+ /**
15690
+ * ホストが `<stateProp>: path`(1 セグメント = 丸ごとマウント・ルート規則)を
15691
+ * このコンポーネントに書いているか。バインディング初期化が済んだ後に呼ぶこと
15692
+ * (`State._initializeBindWebComponent` は `waitInitializeBinding` の後で呼ぶ)。
15693
+ */
15694
+ function hasRootMountBinding(component, stateProp) {
15695
+ if (!component.hasAttribute(config.bindAttributeName)) {
15696
+ return false;
15697
+ }
15698
+ const bindings = getBindingsByNode(component);
15699
+ if (bindings === null) {
15700
+ return false;
15701
+ }
15702
+ for (const binding of bindings) {
15703
+ if (binding.propSegments.length === 1 && binding.propSegments[0] === stateProp) {
15704
+ return true;
15705
+ }
15706
+ }
15707
+ return false;
15708
+ }
15709
+
15269
15710
  function getStateInfo(state) {
15270
15711
  const getterPaths = new Set();
15271
15712
  const setterPaths = new Set();
@@ -15496,6 +15937,11 @@ class State extends HTMLElementBase {
15496
15937
  }
15497
15938
  await this._loadingPromise;
15498
15939
  this._name = this.getAttribute('name') || 'default';
15940
+ if (this.hasAttribute('name') && !this.hasAttribute('bind-component')) {
15941
+ // 名前付き State は v2 でマウント(`mount=`)に置き換わる(docs/state-mount-design.md D16)。
15942
+ // Light DOM の bind-component は今日 name が必須なので、そちらには言わない。
15943
+ warnNamedStateDeprecated('attribute', this._name);
15944
+ }
15499
15945
  setStateElementByName(this.rootNode, this._name, this);
15500
15946
  }
15501
15947
  async _initializeBindWebComponent() {
@@ -15531,6 +15977,11 @@ class State extends HTMLElementBase {
15531
15977
  raiseError(`"bind-component" cannot be combined with ${conflicting.join(", ")}. The component's "${this.getAttribute("bind-component")}" property is the only state source.`);
15532
15978
  }
15533
15979
  const boundComponentStateProp = this.getAttribute("bind-component");
15980
+ // 束ねる意思をここで宣言する(完了はずっと後)。丸ごとマウント `state: user` の
15981
+ // 完了前の初期適用は、この宣言を見て書き込みを抑止する
15982
+ // (webComponent/completeWebComponent.ts)。下の await より前でなければ、
15983
+ // 親の初期適用が先に走って親のオブジェクトを state プロパティに書いてしまう。
15984
+ markWebComponentStatePropDeclared(boundComponent, boundComponentStateProp);
15534
15985
  const componentRegistry = getCustomElementRegistry(boundComponent);
15535
15986
  if (componentRegistry === null) {
15536
15987
  // null レジストリのサブツリーではホストは永久に upgrade されない。
@@ -15545,10 +15996,19 @@ class State extends HTMLElementBase {
15545
15996
  if (!(boundComponentStateProp in boundComponent)) {
15546
15997
  raiseError(`Component does not have property "${boundComponentStateProp}" for state binding.`);
15547
15998
  }
15548
- const state = boundComponent[boundComponentStateProp];
15999
+ let state = boundComponent[boundComponentStateProp];
15549
16000
  if (typeof state !== 'object' || state === null) {
15550
16001
  raiseError(`Component property "${boundComponentStateProp}" is not an object for state binding.`);
15551
16002
  }
16003
+ // 丸ごとマウント(`state: user`)の完了前の初期適用が、宣言より先に走って state
16004
+ // プロパティを親のオブジェクトごと置き換えていたら、作者のオブジェクトに戻す
16005
+ // (webComponent/preCompletionWrites.ts)。戻さないと親のキー全部が own data key に
16006
+ // なり、R1 で全部が私有に化ける。
16007
+ const authored = takeOverwrittenObject(boundComponent, boundComponentStateProp);
16008
+ if (typeof authored !== 'undefined' && hasRootMountBinding(boundComponent, boundComponentStateProp)) {
16009
+ boundComponent[boundComponentStateProp] = authored;
16010
+ state = authored;
16011
+ }
15552
16012
  this._boundComponent = boundComponent;
15553
16013
  this._boundComponentStateProp = boundComponentStateProp;
15554
16014
  bindWebComponent(this, this._boundComponent, this._boundComponentStateProp, state);
@@ -15605,11 +16065,17 @@ class State extends HTMLElementBase {
15605
16065
  }
15606
16066
  // mapped = プライマリ規則が 1 件以上あることと同義(bindWebComponent の分岐)
15607
16067
  const innerPaths = getPrimaryInnerPaths(this._boundComponent);
16068
+ // ルート規則(丸ごとマウント)は内側パスが空なので、子の登録済みパス全部を読み直す
16069
+ // (webComponent/rootReloadPaths.ts)
16070
+ const rootPaths = hasRootMappingRule(this._boundComponent) ? getRootReloadPaths(this) : [];
15608
16071
  resetDerivedMappingRules(this._boundComponent);
15609
16072
  this.createState("readonly", (state) => {
15610
16073
  for (const path of innerPaths) {
15611
16074
  state.$postUpdate(path);
15612
16075
  }
16076
+ for (const path of rootPaths) {
16077
+ state.$postUpdate(path);
16078
+ }
15613
16079
  });
15614
16080
  }
15615
16081
  async _callStateConnectedCallback() {
@@ -15854,6 +16320,9 @@ class State extends HTMLElementBase {
15854
16320
  get hasMappedComponentState() {
15855
16321
  return this._hasMappedComponentState;
15856
16322
  }
16323
+ get boundPaths() {
16324
+ return this._pathSet;
16325
+ }
15857
16326
  /**
15858
16327
  * この state の実体が innerState proxy であることを記録する。唯一の呼び手は
15859
16328
  * `bindWebComponent` の mapped 分岐(§1.8)。