@wcstack/state 1.31.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/README.ja.md +288 -7
- package/README.md +290 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +204 -1
- package/dist/index.esm.js +1479 -148
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +28 -8
- package/dist/parser.esm.js +29 -8
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
3
|
+
// Generated from /protocol/ssr-snapshot.ts by scripts/sync-protocol-types.mjs.
|
|
4
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
5
|
+
// ===========================================================================
|
|
6
|
+
// ssr-snapshot protocol — how the SSR renderer asks whoever owns reactive
|
|
7
|
+
// state to build hydration snapshots (<wcs-ssr>) as a final pass, after every
|
|
8
|
+
// DOM inserter (router route content, late custom elements) has settled.
|
|
9
|
+
//
|
|
10
|
+
// Without this, the snapshot is built inside <wcs-state>'s connectedCallback
|
|
11
|
+
// and races DOM inserted by other packages: whether a route's structural
|
|
12
|
+
// templates make it into the snapshot depends on document order and state's
|
|
13
|
+
// load mechanism (docs/ssr-router-design.md §5).
|
|
14
|
+
//
|
|
15
|
+
// The provider (@wcstack/state) installs itself on a well-known global symbol
|
|
16
|
+
// at bootstrap. The renderer (@wcstack/server) looks the builder up after
|
|
17
|
+
// running bootstraps: if present it announces orchestration by setting
|
|
18
|
+
// `data-wcs-server="orchestrated"` on the document element BEFORE parsing, and
|
|
19
|
+
// calls build() right before serialization. The provider keeps its inline
|
|
20
|
+
// per-element fallback whenever the attribute value is anything else, so:
|
|
21
|
+
// - old renderer + new provider -> inline build, yesterday's behavior
|
|
22
|
+
// - new renderer + old provider -> no builder found, attribute stays "",
|
|
23
|
+
// the old provider builds inline as before
|
|
24
|
+
// - new renderer + new provider -> orchestrated: snapshots are built last
|
|
25
|
+
// and therefore always see settled DOM
|
|
26
|
+
//
|
|
27
|
+
// The symbol (rather than a package import) also pins the builder to the state
|
|
28
|
+
// copy that actually runs on the page — its module-scoped fragment registries
|
|
29
|
+
// are the ones the snapshot must read.
|
|
30
|
+
//
|
|
31
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/ssr-snapshot.ts), then
|
|
32
|
+
// run `node scripts/sync-protocol-types.mjs` to regenerate the per-package
|
|
33
|
+
// copies (packages/<pkg>/src/protocol/ssrSnapshot.ts). Those copies are
|
|
34
|
+
// generated — do not edit them.
|
|
35
|
+
/**
|
|
36
|
+
* Global key the snapshot builder installs itself under. `Symbol.for` so
|
|
37
|
+
* independently loaded copies of this file (state's and server's) still agree.
|
|
38
|
+
*/
|
|
39
|
+
const SSR_SNAPSHOT_BUILDER_KEY = Symbol.for("wcstack.ssr.snapshotBuilder");
|
|
40
|
+
/**
|
|
41
|
+
* `data-wcs-server` attribute value announcing that the renderer will call the
|
|
42
|
+
* builder as a final pass. Providers must skip their inline per-element build
|
|
43
|
+
* when they see this value, and keep it for any other value (including "").
|
|
44
|
+
*/
|
|
45
|
+
const SSR_ORCHESTRATED_VALUE = "orchestrated";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* サーバー主導スナップショット(orchestrated)の判定
|
|
49
|
+
* (docs/ssr-router-design.md §5)。renderToString が snapshot builder を
|
|
50
|
+
* 見つけたときだけ `data-wcs-server="orchestrated"` を宣言する — 値が他の
|
|
51
|
+
* もの(旧 server の "" を含む)なら inline 生成が従来どおり働く。
|
|
52
|
+
* inSsr と同じ理由でキャッシュしない。
|
|
53
|
+
*/
|
|
54
|
+
function isOrchestratedSsr() {
|
|
55
|
+
const html = document.documentElement;
|
|
56
|
+
return html ? html.getAttribute('data-wcs-server') === SSR_ORCHESTRATED_VALUE : false;
|
|
57
|
+
}
|
|
1
58
|
function inSsr() {
|
|
2
59
|
// キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
|
|
3
60
|
// 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
|
|
@@ -1142,16 +1199,33 @@ const fix = (options) => {
|
|
|
1142
1199
|
/**
|
|
1143
1200
|
* Locale number filter - formats number according to locale.
|
|
1144
1201
|
*
|
|
1202
|
+
* ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は
|
|
1203
|
+
* **明示引数だけを構築時に確定し、既定の `config.locale` は適用のたびに読む**。
|
|
1204
|
+
*
|
|
1205
|
+
* 以前は `options?.[0] ?? config.locale` を返り値の関数の**外**で解決していた。
|
|
1206
|
+
* フィルタ関数はバインド構築時に一度だけ作られるので、これはロケールを
|
|
1207
|
+
* クロージャに焼き込むことを意味する。`config.locale` の確定がバインド構築より
|
|
1208
|
+
* 遅れると、それ以降どう直しても「同じページの中で日付だけ既定ロケール」が
|
|
1209
|
+
* 永続し、しかも `config.locale` は依存グラフに載らないので再描画で回復もしない。
|
|
1210
|
+
* 症状(日付だけ英語)は原因(起動順序)から遠く、追いにくい。
|
|
1211
|
+
*
|
|
1212
|
+
* 適用のたびに読めば、少なくとも**再適用されたバインドは回復する**。ロケールは
|
|
1213
|
+
* 起動時に確定する前提(docs/i18n-design.md D1)なので通常この差は現れず、
|
|
1214
|
+
* これは順序事故から復帰できるようにするための保険である。
|
|
1215
|
+
*
|
|
1216
|
+
* 明示引数(`|date(ja-JP)`)は構築時に固定でよい — バインド式の一部であり、
|
|
1217
|
+
* 実行中に変わらない。
|
|
1218
|
+
*
|
|
1145
1219
|
* @param options - Array with locale string as first element (default: config.locale)
|
|
1146
1220
|
* @returns Filter function that returns localized number string
|
|
1147
1221
|
*/
|
|
1148
1222
|
const locale = (options) => {
|
|
1149
|
-
const
|
|
1223
|
+
const explicit = options?.[0];
|
|
1150
1224
|
return (value) => {
|
|
1151
1225
|
if (typeof value !== 'number') {
|
|
1152
1226
|
valueMustBeNumber('locale');
|
|
1153
1227
|
}
|
|
1154
|
-
return value.toLocaleString(
|
|
1228
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1155
1229
|
};
|
|
1156
1230
|
};
|
|
1157
1231
|
/**
|
|
@@ -1464,12 +1538,13 @@ const truncate = (options) => {
|
|
|
1464
1538
|
* @returns Filter function that returns date string
|
|
1465
1539
|
*/
|
|
1466
1540
|
const date = (options) => {
|
|
1467
|
-
|
|
1541
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1542
|
+
const explicit = options?.[0];
|
|
1468
1543
|
return (value) => {
|
|
1469
1544
|
if (!(value instanceof Date)) {
|
|
1470
1545
|
valueMustBeDate('date');
|
|
1471
1546
|
}
|
|
1472
|
-
return value.toLocaleDateString(
|
|
1547
|
+
return value.toLocaleDateString(explicit ?? config.locale);
|
|
1473
1548
|
};
|
|
1474
1549
|
};
|
|
1475
1550
|
/**
|
|
@@ -1479,12 +1554,13 @@ const date = (options) => {
|
|
|
1479
1554
|
* @returns Filter function that returns time string
|
|
1480
1555
|
*/
|
|
1481
1556
|
const time = (options) => {
|
|
1482
|
-
|
|
1557
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1558
|
+
const explicit = options?.[0];
|
|
1483
1559
|
return (value) => {
|
|
1484
1560
|
if (!(value instanceof Date)) {
|
|
1485
1561
|
valueMustBeDate('time');
|
|
1486
1562
|
}
|
|
1487
|
-
return value.toLocaleTimeString(
|
|
1563
|
+
return value.toLocaleTimeString(explicit ?? config.locale);
|
|
1488
1564
|
};
|
|
1489
1565
|
};
|
|
1490
1566
|
/**
|
|
@@ -1494,12 +1570,13 @@ const time = (options) => {
|
|
|
1494
1570
|
* @returns Filter function that returns datetime string
|
|
1495
1571
|
*/
|
|
1496
1572
|
const datetime = (options) => {
|
|
1497
|
-
|
|
1573
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1574
|
+
const explicit = options?.[0];
|
|
1498
1575
|
return (value) => {
|
|
1499
1576
|
if (!(value instanceof Date)) {
|
|
1500
1577
|
valueMustBeDate('datetime');
|
|
1501
1578
|
}
|
|
1502
|
-
return value.toLocaleString(
|
|
1579
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1503
1580
|
};
|
|
1504
1581
|
};
|
|
1505
1582
|
/**
|
|
@@ -1851,6 +1928,32 @@ function parsePropPart(propPart) {
|
|
|
1851
1928
|
};
|
|
1852
1929
|
}
|
|
1853
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
|
+
|
|
1854
1957
|
const cacheFilterInfos = new Map();
|
|
1855
1958
|
// format: statePath@stateName|filter|filter
|
|
1856
1959
|
// statePath-format: path.to.property (e.g., user.name.first, users.*.name, users.0.name, not include @)
|
|
@@ -1877,6 +1980,10 @@ function parseStatePart(statePart) {
|
|
|
1877
1980
|
else {
|
|
1878
1981
|
stateAndPath = statePart.trim();
|
|
1879
1982
|
}
|
|
1983
|
+
if (stateAndPath.indexOf(STATE_NAME_SEPARATOR) !== -1) {
|
|
1984
|
+
// `path@name` は v2 で消える(docs/state-mount-design.md D16。既定では黙る)
|
|
1985
|
+
warnNamedStateDeprecated('path', stateAndPath);
|
|
1986
|
+
}
|
|
1880
1987
|
const [statePathName, stateName = 'default'] = stateAndPath.split(STATE_NAME_SEPARATOR).map(trimFn);
|
|
1881
1988
|
const pathInfo = getPathInfo(statePathName);
|
|
1882
1989
|
return {
|
|
@@ -3942,11 +4049,12 @@ const outerMappingByElement = new WeakMap();
|
|
|
3942
4049
|
const primaryMappingRuleSetByElement = new WeakMap();
|
|
3943
4050
|
const primaryBindingByMappingRule = new WeakMap();
|
|
3944
4051
|
function createMappingRuleByBinding(innerState, binding) {
|
|
3945
|
-
const
|
|
4052
|
+
const innerPath = binding.propSegments.slice(1).join(DELIMITER);
|
|
4053
|
+
const innerPathInfo = getPathInfo(innerPath);
|
|
3946
4054
|
const innerAbsPathInfo = getAbsolutePathInfo(innerState, innerPathInfo);
|
|
3947
4055
|
const outerAbsStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
3948
4056
|
const outerAbsPathInfo = outerAbsStateAddress.absolutePathInfo;
|
|
3949
|
-
return { innerAbsPathInfo, outerAbsPathInfo };
|
|
4057
|
+
return { innerAbsPathInfo, outerAbsPathInfo, isRoot: innerPath.length === 0 };
|
|
3950
4058
|
}
|
|
3951
4059
|
function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
3952
4060
|
if (bindings.length === 0) {
|
|
@@ -3958,8 +4066,16 @@ function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
|
3958
4066
|
}
|
|
3959
4067
|
const innerMappingRule = new Map();
|
|
3960
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();
|
|
3961
4072
|
for (const binding of bindings) {
|
|
3962
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);
|
|
3963
4079
|
let primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
3964
4080
|
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
3965
4081
|
primaryMappingRuleSetByElement.set(webComponent, new Set([mappingRule]));
|
|
@@ -4007,11 +4123,14 @@ function resetDerivedMappingRules(webComponent) {
|
|
|
4007
4123
|
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
4008
4124
|
}
|
|
4009
4125
|
/**
|
|
4010
|
-
*
|
|
4126
|
+
* このコンポーネントに張られたプライマリ規則の**内側パス**を列挙する(ルート規則を除く)。
|
|
4011
4127
|
*
|
|
4012
4128
|
* 切断 → 再接続を跨いだ子(行 content の再利用で起きる)は、切断中に親で起きた変更の
|
|
4013
4129
|
* 通知を受け取れていない。再接続時に「束ねているパスを読み直せ」と撃つための入力で、
|
|
4014
4130
|
* 何が変わったかは分からないのでプライマリ規則の粒度で丸ごと読み直す(§1.9)。
|
|
4131
|
+
*
|
|
4132
|
+
* ルート規則は内側パスが空で `$postUpdate("")` に意味が無いため含めない。ルート規則の
|
|
4133
|
+
* 読み直しは子の登録済みパス全部(`getRootReloadPaths`)で行う。
|
|
4015
4134
|
*/
|
|
4016
4135
|
function getPrimaryInnerPaths(webComponent) {
|
|
4017
4136
|
const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
@@ -4020,13 +4139,37 @@ function getPrimaryInnerPaths(webComponent) {
|
|
|
4020
4139
|
}
|
|
4021
4140
|
const paths = [];
|
|
4022
4141
|
for (const rule of primaryMappingRuleSet) {
|
|
4142
|
+
if (rule.isRoot) {
|
|
4143
|
+
continue;
|
|
4144
|
+
}
|
|
4023
4145
|
paths.push(rule.innerAbsPathInfo.pathInfo.path);
|
|
4024
4146
|
}
|
|
4025
4147
|
return paths;
|
|
4026
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
|
+
}
|
|
4027
4166
|
/**
|
|
4028
4167
|
* 内側のパスを外側のパスへ翻訳する。規則が無ければプライマリ規則から導出する。
|
|
4029
4168
|
*
|
|
4169
|
+
* 導出は**最長接頭辞一致**。ルート規則(接頭辞長 0)はあらゆる内側パスに一致するが、
|
|
4170
|
+
* 部分規則(`state.theme: theme`)がより長い接頭辞で一致すればそちらが勝つ
|
|
4171
|
+
* (`state: rows; state.theme: theme` の併用 — 設計書 §3-2)。
|
|
4172
|
+
*
|
|
4030
4173
|
* `registerSubscriber` は導出に**副作用を持たせるか**の切り替え。既定(子の read /
|
|
4031
4174
|
* write からの呼び出し)では導出した規則を台帳に memo し、対応するバインディングを
|
|
4032
4175
|
* 親スコープの購読者として登録する。`false` を渡すと**参照専用**になり、台帳にも
|
|
@@ -4061,16 +4204,28 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo, registerSubscr
|
|
|
4061
4204
|
return null;
|
|
4062
4205
|
}
|
|
4063
4206
|
let primaryMappingRule = null;
|
|
4207
|
+
let matchedPrefixLength = -1;
|
|
4064
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
|
+
}
|
|
4065
4217
|
// innerPathInfoがprimaryMappingRuleのinnerPathInfoを包含しているか
|
|
4066
4218
|
if (!innerAbsPathInfo.pathInfo.cumulativePathInfoSet.has(currentPrimaryMappingRule.innerAbsPathInfo.pathInfo)) {
|
|
4067
4219
|
continue;
|
|
4068
4220
|
}
|
|
4069
|
-
|
|
4221
|
+
const prefixLength = currentPrimaryMappingRule.innerAbsPathInfo.pathInfo.segments.length;
|
|
4222
|
+
if (prefixLength === innerAbsPathInfo.pathInfo.segments.length) {
|
|
4070
4223
|
raiseError('Duplicate mapping rule for web component.');
|
|
4071
4224
|
}
|
|
4072
|
-
|
|
4073
|
-
|
|
4225
|
+
if (prefixLength > matchedPrefixLength) {
|
|
4226
|
+
primaryMappingRule = currentPrimaryMappingRule;
|
|
4227
|
+
matchedPrefixLength = prefixLength;
|
|
4228
|
+
}
|
|
4074
4229
|
}
|
|
4075
4230
|
if (primaryMappingRule === null) {
|
|
4076
4231
|
// マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
|
|
@@ -4083,7 +4238,7 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo, registerSubscr
|
|
|
4083
4238
|
raiseError('Binding not found for primary mapping rule on web component.');
|
|
4084
4239
|
}
|
|
4085
4240
|
/* c8 ignore stop */
|
|
4086
|
-
const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(
|
|
4241
|
+
const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(matchedPrefixLength);
|
|
4087
4242
|
const outerSegments = primaryMappingRule.outerAbsPathInfo.pathInfo.segments.concat(outerRemainingSegments);
|
|
4088
4243
|
const outerPathInfo = getPathInfo(outerSegments.join(DELIMITER));
|
|
4089
4244
|
const rootNode = webComponent.getRootNode();
|
|
@@ -4568,6 +4723,16 @@ function addInterestedSession(node, session) {
|
|
|
4568
4723
|
}
|
|
4569
4724
|
interestedSessionsByNode.set(node, new Set([current, session]));
|
|
4570
4725
|
}
|
|
4726
|
+
/**
|
|
4727
|
+
* このノードに既にバインドが張られているか。
|
|
4728
|
+
*
|
|
4729
|
+
* binder プロトコル(`bind()`)の冪等判定に使う。`remember` が binding ごとに
|
|
4730
|
+
* `addInterestedSession(binding.replaceNode, …)` を呼ぶので、バインド済みノードは
|
|
4731
|
+
* 必ずこの台帳に載っている。新しい台帳を足さずに済むぶん、二重管理の齟齬が無い。
|
|
4732
|
+
*/
|
|
4733
|
+
function hasInterestedSession(node) {
|
|
4734
|
+
return interestedSessionsByNode.has(node);
|
|
4735
|
+
}
|
|
4571
4736
|
function forEachInterestedSession(node, callback) {
|
|
4572
4737
|
const current = interestedSessionsByNode.get(node);
|
|
4573
4738
|
if (typeof current === "undefined")
|
|
@@ -5583,6 +5748,33 @@ function markWebComponentAsComplete(webComponent, stateProp) {
|
|
|
5583
5748
|
function isWebComponentComplete(webComponent, stateProp) {
|
|
5584
5749
|
return completedStatePropsByWebComponent.get(webComponent)?.has(stateProp) === true;
|
|
5585
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
|
+
}
|
|
5586
5778
|
|
|
5587
5779
|
function applyChangeToAttribute(binding, _context, newValue) {
|
|
5588
5780
|
const element = binding.node;
|
|
@@ -6452,8 +6644,23 @@ class Content {
|
|
|
6452
6644
|
let anchor = targetNode;
|
|
6453
6645
|
for (const node of this._movableNodes()) {
|
|
6454
6646
|
if (anchor.nextSibling !== node) {
|
|
6647
|
+
// moveBefore も childList mutation record を出すため、マークは両分岐の前
|
|
6455
6648
|
markObserverSkipOnAdd(node);
|
|
6456
|
-
|
|
6649
|
+
// moveBefore は取り外しを伴わない移動 — 接続済み行の reorder で
|
|
6650
|
+
// フォーカス・iframe・アニメーション状態を保存する(docs/a11y-design.md §4-1)。
|
|
6651
|
+
// この 1 文は 4 つのノード状態を共有する: (a) 接続済み reorder、
|
|
6652
|
+
// (b) clone フラグメント由来(root 違い)、(c) プール/unmount 済み(親なし)、
|
|
6653
|
+
// (d) バッチフラグメント内。moveBefore は「同一ツリー・親あり」を要求し
|
|
6654
|
+
// (b)(c)(d) では HierarchyRequestError を投げるため、same-parent ガード
|
|
6655
|
+
// (同 root かつ親が非 null の同時証明 = フォーカス保存が意味を持つ (a) と
|
|
6656
|
+
// 正確に一致)は外せない。ガードを外す「簡略化」をしてはならない。
|
|
6657
|
+
const mover = parentNode;
|
|
6658
|
+
if (node.parentNode === parentNode && typeof mover.moveBefore === "function") {
|
|
6659
|
+
mover.moveBefore(node, anchor.nextSibling);
|
|
6660
|
+
}
|
|
6661
|
+
else {
|
|
6662
|
+
parentNode.insertBefore(node, anchor.nextSibling);
|
|
6663
|
+
}
|
|
6457
6664
|
}
|
|
6458
6665
|
anchor = node;
|
|
6459
6666
|
}
|
|
@@ -6665,6 +6872,157 @@ function createContent(bindingInfo) {
|
|
|
6665
6872
|
return content;
|
|
6666
6873
|
}
|
|
6667
6874
|
|
|
6875
|
+
// ===========================================================================
|
|
6876
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
6877
|
+
// Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
|
|
6878
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
6879
|
+
// ===========================================================================
|
|
6880
|
+
// transition-runner protocol — how a package that mutates the DOM hands that
|
|
6881
|
+
// mutation to whoever is arbitrating view transitions on the page.
|
|
6882
|
+
//
|
|
6883
|
+
// @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
|
|
6884
|
+
// (zero runtime dependencies, independently publishable), so the arbiter installs
|
|
6885
|
+
// itself on a well-known global symbol and the participants look it up lazily.
|
|
6886
|
+
// No arbiter installed means the mutation is invoked directly, synchronously —
|
|
6887
|
+
// byte-for-byte the behavior these packages had before the protocol existed.
|
|
6888
|
+
//
|
|
6889
|
+
// docs/view-transition-design.md §4 is the normative description.
|
|
6890
|
+
//
|
|
6891
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
|
|
6892
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
6893
|
+
// (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
|
|
6894
|
+
/**
|
|
6895
|
+
* Global key the arbiter installs itself under. `Symbol.for` so independently
|
|
6896
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
6897
|
+
*/
|
|
6898
|
+
const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
|
|
6899
|
+
/**
|
|
6900
|
+
* The installed arbiter, or null when there is none, it speaks a version this
|
|
6901
|
+
* reader does not, or it does not accept this participant.
|
|
6902
|
+
*
|
|
6903
|
+
* Looked up on every call rather than cached: the tag can be added, removed, or
|
|
6904
|
+
* reconfigured at any point in a page's life, and a stale cache would either
|
|
6905
|
+
* animate what the author just switched off or miss what they switched on.
|
|
6906
|
+
*/
|
|
6907
|
+
function getTransitionRunner(source) {
|
|
6908
|
+
const candidate = globalThis[TRANSITION_RUNNER_KEY];
|
|
6909
|
+
if (candidate === undefined || candidate === null)
|
|
6910
|
+
return null;
|
|
6911
|
+
if (candidate.protocol !== "wcs-transition-runner")
|
|
6912
|
+
return null;
|
|
6913
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
6914
|
+
return null;
|
|
6915
|
+
if (typeof candidate.run !== "function")
|
|
6916
|
+
return null;
|
|
6917
|
+
if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
|
|
6918
|
+
return null;
|
|
6919
|
+
return candidate;
|
|
6920
|
+
}
|
|
6921
|
+
/**
|
|
6922
|
+
* Run `mutate` under the installed arbiter, or directly when there is none.
|
|
6923
|
+
*
|
|
6924
|
+
* Returns `undefined` in the no-arbiter case instead of a resolved promise: the
|
|
6925
|
+
* state drain calls this on every batch, and awaiting is a caller's choice, not
|
|
6926
|
+
* an allocation the common path should pay for. `await` accepts both.
|
|
6927
|
+
*/
|
|
6928
|
+
function runTransition(source, mutate, types) {
|
|
6929
|
+
const runner = getTransitionRunner(source);
|
|
6930
|
+
if (runner === null) {
|
|
6931
|
+
mutate();
|
|
6932
|
+
return undefined;
|
|
6933
|
+
}
|
|
6934
|
+
return runner.run(mutate, { source, types });
|
|
6935
|
+
}
|
|
6936
|
+
|
|
6937
|
+
/** Elements that already carry a generated name (never renamed). */
|
|
6938
|
+
const namedElements = new WeakSet();
|
|
6939
|
+
/**
|
|
6940
|
+
* The generated-name ledger is per *document*, not per module instance.
|
|
6941
|
+
*
|
|
6942
|
+
* `view-transition-name` has to be unique across the whole document: the moment
|
|
6943
|
+
* two elements share one, the browser aborts the transition outright. A
|
|
6944
|
+
* module-scope counter breaks that as soon as `@wcstack/state` is loaded twice on
|
|
6945
|
+
* one page (two CDN bundles), because both copies would start minting
|
|
6946
|
+
* `wcs-row-1`. The transition-runner key is a `Symbol.for` for exactly this
|
|
6947
|
+
* reason, and the counter needs the same protection.
|
|
6948
|
+
*
|
|
6949
|
+
* Sharing the cap is right for the same reason: the cost a cap exists to bound —
|
|
6950
|
+
* one snapshot group per named element — is a document-wide cost, not a
|
|
6951
|
+
* per-bundle one.
|
|
6952
|
+
*/
|
|
6953
|
+
const NAMING_LEDGER_KEY = Symbol.for("wcstack.state.view-transition-naming");
|
|
6954
|
+
function getLedger() {
|
|
6955
|
+
const slot = globalThis;
|
|
6956
|
+
return (slot[NAMING_LEDGER_KEY] ??= { counter: 0, assigned: 0, warned: false });
|
|
6957
|
+
}
|
|
6958
|
+
/**
|
|
6959
|
+
* The active auto-naming policy, or null when names are the author's business
|
|
6960
|
+
* (the default) — one arbiter lookup per structural apply, not per row.
|
|
6961
|
+
*/
|
|
6962
|
+
function getAutoNaming() {
|
|
6963
|
+
const runner = getTransitionRunner("state");
|
|
6964
|
+
if (runner === null || runner.naming !== "auto") {
|
|
6965
|
+
return null;
|
|
6966
|
+
}
|
|
6967
|
+
return { limit: runner.namingLimit };
|
|
6968
|
+
}
|
|
6969
|
+
function firstElementOf(content) {
|
|
6970
|
+
const first = content.firstNode;
|
|
6971
|
+
if (first === null) {
|
|
6972
|
+
return null;
|
|
6973
|
+
}
|
|
6974
|
+
const last = content.lastNode;
|
|
6975
|
+
for (let node = first; node !== null; node = node.nextSibling) {
|
|
6976
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6977
|
+
return node;
|
|
6978
|
+
}
|
|
6979
|
+
if (node === last) {
|
|
6980
|
+
break;
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
return null;
|
|
6984
|
+
}
|
|
6985
|
+
/**
|
|
6986
|
+
* Give this content's first element a unique name plus a class for group
|
|
6987
|
+
* styling, unless it already has one or the cap has been reached.
|
|
6988
|
+
*
|
|
6989
|
+
* The cap exists because every named element becomes its own snapshot group; a
|
|
6990
|
+
* few hundred of them make a transition visibly slow. Past it naming stops and
|
|
6991
|
+
* says so once — silently degrading would leave the author wondering why only
|
|
6992
|
+
* the first part of a list animates.
|
|
6993
|
+
*/
|
|
6994
|
+
function applyTransitionName(content, kind, naming) {
|
|
6995
|
+
const element = firstElementOf(content);
|
|
6996
|
+
if (element === null || namedElements.has(element)) {
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
// A node without `style` (anything outside HTMLElement / SVGElement) cannot
|
|
7000
|
+
// carry a name. Bail before touching the ledger: consuming the cap and marking
|
|
7001
|
+
// the element as named would burn a slot for a name that was never written,
|
|
7002
|
+
// and leave that element permanently ineligible.
|
|
7003
|
+
const style = element.style;
|
|
7004
|
+
if (style === undefined) {
|
|
7005
|
+
return;
|
|
7006
|
+
}
|
|
7007
|
+
const ledger = getLedger();
|
|
7008
|
+
if (ledger.assigned >= naming.limit) {
|
|
7009
|
+
if (!ledger.warned) {
|
|
7010
|
+
ledger.warned = true;
|
|
7011
|
+
console.warn(`[@wcstack/state] auto view-transition-name limit (${naming.limit}) reached; ` +
|
|
7012
|
+
"further elements are left unnamed. Raise naming-limit on <wcs-view-transition>, " +
|
|
7013
|
+
'or switch to naming="manual" and name only what should morph.');
|
|
7014
|
+
}
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
namedElements.add(element);
|
|
7018
|
+
ledger.assigned += 1;
|
|
7019
|
+
ledger.counter += 1;
|
|
7020
|
+
style.setProperty("view-transition-name", `wcs-${kind}-${ledger.counter}`);
|
|
7021
|
+
// Group handle for CSS (`::view-transition-group(*.wcs-row)`). Ignored by
|
|
7022
|
+
// engines that predate view-transition-class, which costs nothing.
|
|
7023
|
+
style.setProperty("view-transition-class", `wcs-${kind}`);
|
|
7024
|
+
}
|
|
7025
|
+
|
|
6668
7026
|
const lastNodeByNode = new WeakMap();
|
|
6669
7027
|
const contentByListIndexByNode = new WeakMap();
|
|
6670
7028
|
const pooledContentsByNode = new WeakMap();
|
|
@@ -6832,6 +7190,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6832
7190
|
setRootNodeByFragment(fragment, context.rootNode);
|
|
6833
7191
|
}
|
|
6834
7192
|
const ssrMode = inSsr();
|
|
7193
|
+
// 自動命名ポリシーは行ごとではなく apply ごとに 1 回だけ引く
|
|
7194
|
+
// (docs/view-transition-design.md §6)。既定の manual では null で、
|
|
7195
|
+
// 以降の行ループは分岐 1 つ分しか増えない。
|
|
7196
|
+
const autoNaming = getAutoNaming();
|
|
6835
7197
|
const uuid = bindingInfo.uuid ?? '';
|
|
6836
7198
|
// 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
|
|
6837
7199
|
// 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
|
|
@@ -6875,6 +7237,9 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6875
7237
|
}
|
|
6876
7238
|
// コンテントを活性化
|
|
6877
7239
|
activateContent(content, loopContext, context);
|
|
7240
|
+
if (autoNaming !== null) {
|
|
7241
|
+
applyTransitionName(content, "row", autoNaming);
|
|
7242
|
+
}
|
|
6878
7243
|
});
|
|
6879
7244
|
if (typeof content === 'undefined') {
|
|
6880
7245
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
@@ -6974,6 +7339,11 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
6974
7339
|
}
|
|
6975
7340
|
const loopContext = getLoopContextByNode(bindingInfo.node);
|
|
6976
7341
|
activateContent(content, loopContext, context);
|
|
7342
|
+
// 自動命名(docs/view-transition-design.md §6)。manual(既定)では null。
|
|
7343
|
+
const autoNaming = getAutoNaming();
|
|
7344
|
+
if (autoNaming !== null) {
|
|
7345
|
+
applyTransitionName(content, "branch", autoNaming);
|
|
7346
|
+
}
|
|
6977
7347
|
}
|
|
6978
7348
|
}
|
|
6979
7349
|
|
|
@@ -7027,6 +7397,68 @@ function applyMirrorAttribute(element, attributeName, value) {
|
|
|
7027
7397
|
element.setAttribute(attributeName, formatted);
|
|
7028
7398
|
}
|
|
7029
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
|
+
|
|
7030
7462
|
/**
|
|
7031
7463
|
* SSR 時に HTML 属性で表現できないプロパティバインディングを蓄積するストア。
|
|
7032
7464
|
* ハイドレーション時にクライアント側で復元する。
|
|
@@ -7123,7 +7555,17 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
7123
7555
|
const propSegments = binding.propSegments;
|
|
7124
7556
|
if (propSegments.length === 1) {
|
|
7125
7557
|
const firstSegment = propSegments[0];
|
|
7126
|
-
|
|
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
|
+
}
|
|
7127
7569
|
const performWrite = () => {
|
|
7128
7570
|
let propertyWriteSucceeded = false;
|
|
7129
7571
|
try {
|
|
@@ -7225,7 +7667,8 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
7225
7667
|
}
|
|
7226
7668
|
subObject = subObject[segment];
|
|
7227
7669
|
}
|
|
7228
|
-
const
|
|
7670
|
+
const lastSegment = propSegments[propSegments.length - 1];
|
|
7671
|
+
const oldValue = subObject[lastSegment];
|
|
7229
7672
|
if (oldValue !== newValue) {
|
|
7230
7673
|
if (Object.isFrozen(subObject)) {
|
|
7231
7674
|
if (config.debug) {
|
|
@@ -7238,8 +7681,15 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
7238
7681
|
}
|
|
7239
7682
|
return;
|
|
7240
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
|
+
}
|
|
7241
7691
|
try {
|
|
7242
|
-
subObject[
|
|
7692
|
+
subObject[lastSegment] = newValue;
|
|
7243
7693
|
}
|
|
7244
7694
|
catch (error) {
|
|
7245
7695
|
if (config.debug) {
|
|
@@ -7297,6 +7747,34 @@ function applyChangeToText(binding, _context, newValue) {
|
|
|
7297
7747
|
}
|
|
7298
7748
|
}
|
|
7299
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
|
+
|
|
7300
7778
|
/**
|
|
7301
7779
|
* 親 state → `bind-component` 済みコンポーネントの再読込通知(内部チャネル)。
|
|
7302
7780
|
*
|
|
@@ -7315,13 +7793,14 @@ function applyChangeToText(binding, _context, newValue) {
|
|
|
7315
7793
|
* (apply/applyChange.ts)、`bindWebComponent` は完了済み = state element は登録済み。
|
|
7316
7794
|
* ただし**登録済みと使用可能は別**で、切断済みの state element が台帳に残っている
|
|
7317
7795
|
* 窓がある(§1.9)。下の使用可能判定を参照。
|
|
7796
|
+
*
|
|
7797
|
+
* 残余パスが空(`data-wcs="state: user"` — ルート規則の丸ごとマウント)は、親が
|
|
7798
|
+
* マウント先を丸ごと差し替えたという通知。何が変わったかは分からないので、子の
|
|
7799
|
+
* 登録済みパス全部を読み直す(docs/state-mount-design.md §3-2 / impl-plan P1-2)。
|
|
7318
7800
|
*/
|
|
7319
7801
|
function applyChangeToWebComponent(binding, _context, _newValue) {
|
|
7320
7802
|
const element = binding.node;
|
|
7321
7803
|
const propSegments = binding.propSegments;
|
|
7322
|
-
if (propSegments.length <= 1) {
|
|
7323
|
-
raiseError(`Invalid propSegments for web component binding: ${propSegments.join(DELIMITER)}`);
|
|
7324
|
-
}
|
|
7325
7804
|
const [firstSegment, ...restSegments] = propSegments;
|
|
7326
7805
|
const innerStateElement = getStateElementByWebComponent(element, firstSegment);
|
|
7327
7806
|
if (innerStateElement === null) {
|
|
@@ -7346,6 +7825,18 @@ function applyChangeToWebComponent(binding, _context, _newValue) {
|
|
|
7346
7825
|
}
|
|
7347
7826
|
return;
|
|
7348
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
|
+
}
|
|
7349
7840
|
innerStateElement.createState("readonly", (state) => {
|
|
7350
7841
|
state.$postUpdate(restSegments.join(DELIMITER));
|
|
7351
7842
|
});
|
|
@@ -7509,12 +8000,47 @@ function missingRootPathMessage(stateName, path, target, declaredPaths) {
|
|
|
7509
8000
|
* 決まるので、噛み合わないことは常にプログラマのミス。
|
|
7510
8001
|
*/
|
|
7511
8002
|
function indexArityMessage(api, path, wildcardCount, actual) {
|
|
8003
|
+
// `$getAll` / `$setAll` の添字は前方一致の接頭辞なので上限、`$resolve` だけが厳密一致
|
|
8004
|
+
// (docs/state-set-all-design.md §4)。
|
|
7512
8005
|
const requirement = api === "$resolve"
|
|
7513
8006
|
? `exactly ${wildcardCount}`
|
|
7514
8007
|
: `at most ${wildcardCount}`;
|
|
7515
8008
|
return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
|
|
7516
8009
|
`("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
|
|
7517
8010
|
}
|
|
8011
|
+
/**
|
|
8012
|
+
* `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
|
|
8013
|
+
* 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
|
|
8014
|
+
* 文脈が添字を持っている場合、黙って全展開に倒すと「文脈で絞られている」という
|
|
8015
|
+
* 書き手の期待と食い違い、異なる文脈の添字の流用とも区別が付かないため throw する。
|
|
8016
|
+
*
|
|
8017
|
+
* 実行時の評価文脈に依存する(`$setAll` の spread 長と同種)ので lint へは誘導しない。
|
|
8018
|
+
*/
|
|
8019
|
+
function getAllContextMismatchMessage(path, contextPath) {
|
|
8020
|
+
return `$getAll("${path}") was called without indexes inside the loop context of ` +
|
|
8021
|
+
`"${contextPath}", but the path shares no wildcard level with that context, ` +
|
|
8022
|
+
`so the context indexes ($1..$n) do not apply. ` +
|
|
8023
|
+
`Pass indexes explicitly ([] expands every level).`;
|
|
8024
|
+
}
|
|
8025
|
+
/**
|
|
8026
|
+
* `$setAll(path, indexes, values, { spread: true })` の配列長がマッチ件数と噛み合わない。
|
|
8027
|
+
*
|
|
8028
|
+
* 静的には件数が分からない(実行時のリスト長に依存する)ので lint へは誘導しない。
|
|
8029
|
+
* 黙って切り詰める/余りを捨てると誤配が通ってしまうため throw する
|
|
8030
|
+
* (docs/state-set-all-design.md §3-3)。
|
|
8031
|
+
*/
|
|
8032
|
+
function setAllSpreadArityMessage(path, matched, actual) {
|
|
8033
|
+
return `$setAll("${path}", …, { spread: true }) requires the values array to have ` +
|
|
8034
|
+
`exactly one entry per matched address (matched ${matched}) but got ${actual}. ` +
|
|
8035
|
+
`Did the list change between $getAll and $setAll?`;
|
|
8036
|
+
}
|
|
8037
|
+
/**
|
|
8038
|
+
* `$setAll` の値と `options` の組み合わせが意味を成さない。
|
|
8039
|
+
* (docs/state-set-all-design.md §3-1)
|
|
8040
|
+
*/
|
|
8041
|
+
function setAllValueKindMessage(path, reason) {
|
|
8042
|
+
return `$setAll("${path}") ${reason}`;
|
|
8043
|
+
}
|
|
7518
8044
|
/**
|
|
7519
8045
|
* ワイルドカードを解決するループ文脈が足りない(=パスの階数 > スコープの階数)。
|
|
7520
8046
|
*
|
|
@@ -7697,21 +8223,36 @@ const deferredSelectBindingByBinding = new WeakMap();
|
|
|
7697
8223
|
// registry 照会を省略できる。scoped registry を導入する場合はこの不可逆前提を再検討。
|
|
7698
8224
|
const definedApplyVerifiedByBinding = new WeakMap();
|
|
7699
8225
|
/**
|
|
7700
|
-
*
|
|
7701
|
-
*
|
|
7702
|
-
*
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
*
|
|
7707
|
-
*
|
|
7708
|
-
*
|
|
7709
|
-
*
|
|
7710
|
-
*
|
|
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` の完了前の積みも含む)
|
|
8241
|
+
*
|
|
8242
|
+
* 以前は 1 セグメントを通知チャネルから除いていた(残余が空だと applyChangeToWebComponent
|
|
8243
|
+
* が raiseError し、updater の drain が捕まえないので同じバッチの無関係な更新まで
|
|
8244
|
+
* 巻き添えにした)。残余空がルート規則の意味を持った今、その除外は要らない。
|
|
7711
8245
|
*/
|
|
7712
|
-
function
|
|
7713
|
-
|
|
7714
|
-
|
|
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;
|
|
7715
8256
|
}
|
|
7716
8257
|
function _applyChange(binding, context) {
|
|
7717
8258
|
const value = getValue(context.state, binding);
|
|
@@ -7726,13 +8267,10 @@ function _applyChange(binding, context) {
|
|
|
7726
8267
|
return;
|
|
7727
8268
|
}
|
|
7728
8269
|
if (fnByBinding.has(binding)) {
|
|
7729
|
-
|
|
7730
|
-
|
|
8270
|
+
fn = resolveCustomElementApply(binding);
|
|
8271
|
+
if (fn === applyChangeToWebComponent) {
|
|
7731
8272
|
fnByBinding.set(binding, fn); // 確定したのでキャッシュ
|
|
7732
8273
|
}
|
|
7733
|
-
else {
|
|
7734
|
-
fn = applyChangeToProperty;
|
|
7735
|
-
}
|
|
7736
8274
|
fn(binding, context, filteredValue);
|
|
7737
8275
|
return;
|
|
7738
8276
|
}
|
|
@@ -7744,13 +8282,10 @@ function _applyChange(binding, context) {
|
|
|
7744
8282
|
if (typeof fn === 'undefined') {
|
|
7745
8283
|
const customTag = getCustomElement(binding.replaceNode);
|
|
7746
8284
|
if (customTag) {
|
|
7747
|
-
|
|
7748
|
-
|
|
8285
|
+
fn = resolveCustomElementApply(binding);
|
|
8286
|
+
if (fn === applyChangeToWebComponent) {
|
|
7749
8287
|
fnByBinding.set(binding, fn); // 確定したのでキャッシュ
|
|
7750
8288
|
}
|
|
7751
|
-
else {
|
|
7752
|
-
fn = applyChangeToProperty;
|
|
7753
|
-
}
|
|
7754
8289
|
}
|
|
7755
8290
|
else {
|
|
7756
8291
|
fn = applyChangeToProperty;
|
|
@@ -8453,7 +8988,7 @@ async function buildBindings(root) {
|
|
|
8453
8988
|
}
|
|
8454
8989
|
}
|
|
8455
8990
|
|
|
8456
|
-
var version = "1.
|
|
8991
|
+
var version = "1.33.0";
|
|
8457
8992
|
var pkg = {
|
|
8458
8993
|
version: version};
|
|
8459
8994
|
|
|
@@ -9176,6 +9711,221 @@ async function hydrateBindings(root) {
|
|
|
9176
9711
|
return true;
|
|
9177
9712
|
}
|
|
9178
9713
|
|
|
9714
|
+
// ===========================================================================
|
|
9715
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
9716
|
+
// Generated from /protocol/binder.ts by scripts/sync-protocol-types.mjs.
|
|
9717
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
9718
|
+
// ===========================================================================
|
|
9719
|
+
// binder protocol — how a package that inserts DOM hands those nodes to whoever
|
|
9720
|
+
// owns data bindings on the page.
|
|
9721
|
+
//
|
|
9722
|
+
// The dual of transition-runner: that one hands a *mutation* to whoever animates
|
|
9723
|
+
// it, this one hands *new nodes* to whoever binds them.
|
|
9724
|
+
//
|
|
9725
|
+
// A `data-wcs` binding exists only for nodes @wcstack/state walked when it built
|
|
9726
|
+
// its bindings. Nodes that arrive later — the content of a route that was not
|
|
9727
|
+
// active at that moment, a <wcs-head> child reflected into <head> — were never
|
|
9728
|
+
// walked, so their bindings silently do nothing, however often they are inserted.
|
|
9729
|
+
// @wcstack/router must not depend on @wcstack/state (zero runtime dependencies,
|
|
9730
|
+
// independently publishable), so state installs a binder on a well-known global
|
|
9731
|
+
// symbol and inserters look it up lazily.
|
|
9732
|
+
//
|
|
9733
|
+
// No binder installed means nothing happens — byte-for-byte the behavior these
|
|
9734
|
+
// packages had before the protocol existed.
|
|
9735
|
+
//
|
|
9736
|
+
// docs/binder-protocol-design.md is the normative description.
|
|
9737
|
+
//
|
|
9738
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/binder.ts), then run
|
|
9739
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
9740
|
+
// (packages/<pkg>/src/protocol/binder.ts). Those copies are generated — do not edit them.
|
|
9741
|
+
/**
|
|
9742
|
+
* Global key the binder installs itself under. `Symbol.for` so independently
|
|
9743
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
9744
|
+
*/
|
|
9745
|
+
const BINDER_KEY = Symbol.for("wcstack.binder");
|
|
9746
|
+
/**
|
|
9747
|
+
* The installed binder, or null when there is none or it speaks a version this
|
|
9748
|
+
* reader does not.
|
|
9749
|
+
*
|
|
9750
|
+
* Looked up on every call rather than cached, for the same reason
|
|
9751
|
+
* transition-runner does: the page's composition can change at any point, and a
|
|
9752
|
+
* stale cache would keep calling into a binder that is no longer there.
|
|
9753
|
+
*/
|
|
9754
|
+
function getBinder() {
|
|
9755
|
+
const candidate = globalThis[BINDER_KEY];
|
|
9756
|
+
if (candidate === undefined || candidate === null)
|
|
9757
|
+
return null;
|
|
9758
|
+
if (candidate.protocol !== "wcs-binder")
|
|
9759
|
+
return null;
|
|
9760
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
9761
|
+
return null;
|
|
9762
|
+
if (typeof candidate.bind !== "function")
|
|
9763
|
+
return null;
|
|
9764
|
+
return candidate;
|
|
9765
|
+
}
|
|
9766
|
+
/**
|
|
9767
|
+
* Subtrees offered before a binder existed, and the set of everything a binder
|
|
9768
|
+
* has taken. Both live on global symbols so that independently loaded copies of
|
|
9769
|
+
* this file — the router's and state's — share one queue.
|
|
9770
|
+
*
|
|
9771
|
+
* The queue is needed because of load order: the router's auto bundle runs
|
|
9772
|
+
* before state's, so `<wcs-head>` reflects its children into `<head>` while
|
|
9773
|
+
* there is still nothing to bind them. Offering them to a binder that arrives
|
|
9774
|
+
* later is the difference between working and silently blank.
|
|
9775
|
+
*/
|
|
9776
|
+
const PENDING_KEY = Symbol.for("wcstack.binder.pending");
|
|
9777
|
+
const TAKEN_KEY = Symbol.for("wcstack.binder.taken");
|
|
9778
|
+
function pendingQueue() {
|
|
9779
|
+
const globals = globalThis;
|
|
9780
|
+
let queue = globals[PENDING_KEY];
|
|
9781
|
+
if (queue === undefined) {
|
|
9782
|
+
queue = [];
|
|
9783
|
+
globals[PENDING_KEY] = queue;
|
|
9784
|
+
}
|
|
9785
|
+
return queue;
|
|
9786
|
+
}
|
|
9787
|
+
function takenSet() {
|
|
9788
|
+
const globals = globalThis;
|
|
9789
|
+
let taken = globals[TAKEN_KEY];
|
|
9790
|
+
if (taken === undefined) {
|
|
9791
|
+
taken = new WeakSet();
|
|
9792
|
+
globals[TAKEN_KEY] = taken;
|
|
9793
|
+
}
|
|
9794
|
+
return taken;
|
|
9795
|
+
}
|
|
9796
|
+
/**
|
|
9797
|
+
* Bind everything offered before this binder existed. Called by the binder right
|
|
9798
|
+
* after it installs itself.
|
|
9799
|
+
*/
|
|
9800
|
+
function flushPendingBinds() {
|
|
9801
|
+
const binder = getBinder();
|
|
9802
|
+
if (binder === null)
|
|
9803
|
+
return;
|
|
9804
|
+
const queue = pendingQueue();
|
|
9805
|
+
if (queue.length === 0)
|
|
9806
|
+
return;
|
|
9807
|
+
const pending = queue.splice(0, queue.length);
|
|
9808
|
+
const taken = takenSet();
|
|
9809
|
+
for (const subtree of pending) {
|
|
9810
|
+
taken.add(subtree);
|
|
9811
|
+
binder.bind(subtree);
|
|
9812
|
+
}
|
|
9813
|
+
}
|
|
9814
|
+
|
|
9815
|
+
/**
|
|
9816
|
+
* binder プロトコルの提供側(docs/binder-protocol-design.md)。
|
|
9817
|
+
*
|
|
9818
|
+
* `buildBindings` は起動時に `document.body` を 1 回走査するだけなので、そのとき
|
|
9819
|
+
* document に居なかったノードのバインドは存在しない。router が後から差し込む
|
|
9820
|
+
* ルート内容や `<wcs-head>` のクローンがこれに当たり、書いたバインドが黙って
|
|
9821
|
+
* 何もしない状態になっていた。`bind()` はその取りこぼしを 1 サブツリー分だけ
|
|
9822
|
+
* 埋める。
|
|
9823
|
+
*
|
|
9824
|
+
* **走査を勝手に広げない。** MutationObserver が見た全追加ノードを走査する形に
|
|
9825
|
+
* すると、バインドを 1 個も持たない挿入(大多数)にコストが乗り、さらに
|
|
9826
|
+
* `innerHTML` で入れた外部由来の DOM が `data-wcs` を発火させることになる。
|
|
9827
|
+
* ここで束ねるのは**明示的に渡されたものだけ**である。
|
|
9828
|
+
*/
|
|
9829
|
+
const BIND_ATTRIBUTE_SELECTOR = () => `[${config.bindAttributeName}]`;
|
|
9830
|
+
/**
|
|
9831
|
+
* このサブツリーは既にバインド済みか。
|
|
9832
|
+
*
|
|
9833
|
+
* ルート内容は「起動時に active だったので全部バインド済み」か「一度も走査されて
|
|
9834
|
+
* いないので全部未バインド」のどちらかで、途中の状態を取らない。したがって
|
|
9835
|
+
* **宣言を持つ最初のノード 1 個**を見れば足りる。全ノードを走査して判定するのは
|
|
9836
|
+
* 同じ結論により高いコストを払うだけになる。
|
|
9837
|
+
*/
|
|
9838
|
+
function alreadyBound(subtree) {
|
|
9839
|
+
if (hasInterestedSession(subtree)) {
|
|
9840
|
+
return true;
|
|
9841
|
+
}
|
|
9842
|
+
if (!isElement(subtree)) {
|
|
9843
|
+
return false;
|
|
9844
|
+
}
|
|
9845
|
+
if (subtree.hasAttribute(config.bindAttributeName)) {
|
|
9846
|
+
// 属性を持つのに台帳に居ない = 未バインド
|
|
9847
|
+
return false;
|
|
9848
|
+
}
|
|
9849
|
+
const first = subtree.querySelector(BIND_ATTRIBUTE_SELECTOR());
|
|
9850
|
+
return first !== null && hasInterestedSession(first);
|
|
9851
|
+
}
|
|
9852
|
+
function isElement(node) {
|
|
9853
|
+
return node.nodeType === 1;
|
|
9854
|
+
}
|
|
9855
|
+
function bindNow(subtree) {
|
|
9856
|
+
if (alreadyBound(subtree)) {
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9859
|
+
convertMustacheToComments(subtree);
|
|
9860
|
+
collectStructuralFragments(subtree.getRootNode(), subtree);
|
|
9861
|
+
// `getSubscriberNodes` の TreeWalker は**ルート自身を返さない**。`buildBindings` は
|
|
9862
|
+
// `document.body` を渡すので今まで問題にならなかったが、ここには宣言をルートに
|
|
9863
|
+
// 持つノードが来る(`<wcs-head>` が head へ入れる `<title data-wcs="…">`)。
|
|
9864
|
+
// そのときだけ親から走査して、ルートを走査範囲に含める。兄弟の重複登録は
|
|
9865
|
+
// `registeredNodeSet` が弾くので、余計なバインドは生まれない。
|
|
9866
|
+
// 親は Element とは限らない(ShadowRoot 直下なら DocumentFragment、head 直下なら
|
|
9867
|
+
// Element)。`parentElement` だと前者で null になり、ルートを含められない。
|
|
9868
|
+
const declaresOnRoot = subtree.hasAttribute(config.bindAttributeName);
|
|
9869
|
+
const parent = subtree.parentNode;
|
|
9870
|
+
const canWalkFromParent = parent !== null
|
|
9871
|
+
&& (parent.nodeType === 1 || parent.nodeType === 9 || parent.nodeType === 11);
|
|
9872
|
+
const walkRoot = declaresOnRoot && canWalkFromParent
|
|
9873
|
+
? parent
|
|
9874
|
+
: subtree;
|
|
9875
|
+
initializeBindings(walkRoot, null);
|
|
9876
|
+
}
|
|
9877
|
+
/**
|
|
9878
|
+
* 初期バインド構築より前に差し出されたサブツリー。
|
|
9879
|
+
*
|
|
9880
|
+
* `<wcs-head>` は `connectedCallback` の中でクローンを head へ入れるので、
|
|
9881
|
+
* state / router のどちらを先に読み込んでも「まだ構築が終わっていない」時点で
|
|
9882
|
+
* bind を求めてくる。そこで同期に束ねても `<wcs-state>` の登録が済んでおらず、
|
|
9883
|
+
* バインドは state を見つけられない。**構築の完了を唯一の合図にする。**
|
|
9884
|
+
*/
|
|
9885
|
+
const beforeFirstBuild = [];
|
|
9886
|
+
function bind(subtree) {
|
|
9887
|
+
if (!isElement(subtree) || alreadyBound(subtree)) {
|
|
9888
|
+
return;
|
|
9889
|
+
}
|
|
9890
|
+
if (!areBindingsBuilt(subtree.getRootNode())) {
|
|
9891
|
+
beforeFirstBuild.push(subtree);
|
|
9892
|
+
return;
|
|
9893
|
+
}
|
|
9894
|
+
bindNow(subtree);
|
|
9895
|
+
}
|
|
9896
|
+
/**
|
|
9897
|
+
* 初期バインド構築の完了時に呼ぶ(stateElementByName.ts)。binder が居ない時点で
|
|
9898
|
+
* 差し出された分(プロトコルの保留キュー)と、居たが早すぎた分をまとめて束ねる。
|
|
9899
|
+
*/
|
|
9900
|
+
function drainPendingBinds() {
|
|
9901
|
+
const pending = beforeFirstBuild.splice(0, beforeFirstBuild.length);
|
|
9902
|
+
for (const subtree of pending) {
|
|
9903
|
+
bindNow(subtree);
|
|
9904
|
+
}
|
|
9905
|
+
flushPendingBinds();
|
|
9906
|
+
}
|
|
9907
|
+
const binder = {
|
|
9908
|
+
protocol: "wcs-binder",
|
|
9909
|
+
version: 1,
|
|
9910
|
+
bind,
|
|
9911
|
+
};
|
|
9912
|
+
/**
|
|
9913
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
9914
|
+
*
|
|
9915
|
+
* 既に別のコピーが載っているなら譲る。1 ページに 2 つの state バンドルが載る構成
|
|
9916
|
+
* (CDN の取り違え)で、後から読まれた側が先客を追い出すと、先客がバインドした
|
|
9917
|
+
* ノードの台帳と食い違う。
|
|
9918
|
+
*/
|
|
9919
|
+
function registerBinder() {
|
|
9920
|
+
const globals = globalThis;
|
|
9921
|
+
if (globals[BINDER_KEY] === undefined) {
|
|
9922
|
+
globals[BINDER_KEY] = binder;
|
|
9923
|
+
}
|
|
9924
|
+
// ここでは引き取らない。`<wcs-state>` の登録は connectedCallback の await より
|
|
9925
|
+
// 後なので、この時点ではまだ state が居ない。保留分は初期バインド構築の完了時に
|
|
9926
|
+
// 流す(stateElementByName.ts)。そこが「state が確実に居る」最初の瞬間である。
|
|
9927
|
+
}
|
|
9928
|
+
|
|
9179
9929
|
const stateElementByNameByNode = new WeakMap();
|
|
9180
9930
|
const bindingsReadyByNode = new WeakMap();
|
|
9181
9931
|
// devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
|
|
@@ -9198,6 +9948,25 @@ function getStateElementByName(rootNode, name) {
|
|
|
9198
9948
|
function getBindingsReady(rootNode) {
|
|
9199
9949
|
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
9200
9950
|
}
|
|
9951
|
+
const bindingsBuiltRoots = new WeakSet();
|
|
9952
|
+
/**
|
|
9953
|
+
* この rootNode の初期バインド構築が完了しているか。
|
|
9954
|
+
*
|
|
9955
|
+
* binder プロトコル(`bind()`)が使う。router の `<wcs-head>` はクローンを
|
|
9956
|
+
* `connectedCallback` の中で head へ入れるので、**state が最初の走査を終える前**に
|
|
9957
|
+
* bind を求めてくる。そこで同期に束ねても state 要素の初期化が済んでおらず、
|
|
9958
|
+
* 結果は空のままになる。完了までは binder 側で保留する。
|
|
9959
|
+
*
|
|
9960
|
+
* 「まだ登録も済んでいない」と「もう構築が終わった」を取り違えないよう、判定は
|
|
9961
|
+
* 完了の側で持つ。<wcs-state> の登録は connectedCallback の await より後に起きるので、
|
|
9962
|
+
* 「エントリの有無」で進行中かを測ると読み込み順によって逆の答えを返す。
|
|
9963
|
+
*/
|
|
9964
|
+
function areBindingsBuilt(rootNode) {
|
|
9965
|
+
return bindingsBuiltRoots.has(rootNode);
|
|
9966
|
+
}
|
|
9967
|
+
function markBindingsBuilt(rootNode) {
|
|
9968
|
+
bindingsBuiltRoots.add(rootNode);
|
|
9969
|
+
}
|
|
9201
9970
|
function setStateElementByName(rootNode, name, element) {
|
|
9202
9971
|
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
9203
9972
|
if (element === null) {
|
|
@@ -9250,6 +10019,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
9250
10019
|
else {
|
|
9251
10020
|
await buildBindings(rootNode);
|
|
9252
10021
|
}
|
|
10022
|
+
markBindingsBuilt(rootNode);
|
|
10023
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
10024
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
10025
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
10026
|
+
drainPendingBinds();
|
|
9253
10027
|
resolve();
|
|
9254
10028
|
}
|
|
9255
10029
|
catch (error) {
|
|
@@ -9264,6 +10038,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
9264
10038
|
queueMicrotask(async () => {
|
|
9265
10039
|
try {
|
|
9266
10040
|
await buildBindings(rootNode);
|
|
10041
|
+
markBindingsBuilt(rootNode);
|
|
10042
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
10043
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
10044
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
10045
|
+
drainPendingBinds();
|
|
9267
10046
|
resolve();
|
|
9268
10047
|
}
|
|
9269
10048
|
catch (error) {
|
|
@@ -9373,6 +10152,17 @@ function notifyUpdateBatchListeners(batch) {
|
|
|
9373
10152
|
registered.listener(batch);
|
|
9374
10153
|
}
|
|
9375
10154
|
}
|
|
10155
|
+
/**
|
|
10156
|
+
* 遷移越しの適用が失敗したときの報告。
|
|
10157
|
+
*
|
|
10158
|
+
* 遷移の中では例外を同期的に呼び出し元へ投げ返せない。今日の drain は
|
|
10159
|
+
* queueMicrotask の中で throw する = uncaught として観測されるので、それと同じ
|
|
10160
|
+
* 「loud に出す」挙動へ揃える。握り潰すと `$updatedCallback` の throw が黙って
|
|
10161
|
+
* 消える(README の 3 層表が定める伝播の契約が破れる)。
|
|
10162
|
+
*/
|
|
10163
|
+
function reportDeferredApplyFailure(error) {
|
|
10164
|
+
queueMicrotask(() => { throw error; });
|
|
10165
|
+
}
|
|
9376
10166
|
class Updater {
|
|
9377
10167
|
_queueUpdateRecords = [];
|
|
9378
10168
|
constructor() {
|
|
@@ -9481,12 +10271,34 @@ class Updater {
|
|
|
9481
10271
|
// 限られるが、そのとき drain フックまで道連れにすると「機構間の順序は固定」
|
|
9482
10272
|
// (README の 3 層表)が黙って破れる。例外は握らない = 伝播は維持する。
|
|
9483
10273
|
try {
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
10274
|
+
const applyBindings = () => {
|
|
10275
|
+
// context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
|
|
10276
|
+
if (propagationContextByBinding.size > 0) {
|
|
10277
|
+
applyChangeFromBindings(processBindings, propagationContextByBinding);
|
|
10278
|
+
}
|
|
10279
|
+
else {
|
|
10280
|
+
applyChangeFromBindings(processBindings);
|
|
10281
|
+
}
|
|
10282
|
+
};
|
|
10283
|
+
// View transition 参加点(docs/view-transition-design.md §7.2)。arbiter が
|
|
10284
|
+
// 居なければ runTransition はその場で applyBindings を呼び、undefined を返す
|
|
10285
|
+
// = 従来と完全に同じ同期適用。SSR では遷移そのものを持たない(G5)。
|
|
10286
|
+
//
|
|
10287
|
+
// 適用する binding が 0 本のバッチは arbiter へ渡さない。書き込みはバインドの
|
|
10288
|
+
// 有無に関わらず enqueue される(setByAddress)ため、headless なパス
|
|
10289
|
+
// (`$watch` 専用・`$streams` の内部状態・リスト置換の中間アドレス)への
|
|
10290
|
+
// 書き込みだけでもここへ到達する。それでページ全体をスナップショットするのは
|
|
10291
|
+
// 無駄なだけでなく、既定の mode="latest" では「アニメーションすべき DOM 変更が
|
|
10292
|
+
// 無い遷移」が実行中の本物の遷移をスキップしてしまう(ルート遷移が毎回途中で
|
|
10293
|
+
// 切れる/active が空撃ちで振動する)。
|
|
10294
|
+
if (inSsr() || processBindings.length === 0) {
|
|
10295
|
+
applyBindings();
|
|
10296
|
+
}
|
|
9488
10297
|
else {
|
|
9489
|
-
|
|
10298
|
+
const pending = runTransition("state", applyBindings);
|
|
10299
|
+
if (pending !== undefined) {
|
|
10300
|
+
pending.catch(reportDeferredApplyFailure);
|
|
10301
|
+
}
|
|
9490
10302
|
}
|
|
9491
10303
|
}
|
|
9492
10304
|
finally {
|
|
@@ -9881,6 +10693,58 @@ function registerDevtoolsSource() {
|
|
|
9881
10693
|
getOrCreateHookRegistry().register(source);
|
|
9882
10694
|
}
|
|
9883
10695
|
|
|
10696
|
+
/**
|
|
10697
|
+
* ssr-snapshot プロトコルの提供側(docs/ssr-router-design.md §5)。
|
|
10698
|
+
*
|
|
10699
|
+
* `<wcs-ssr>` スナップショットを document 全体に対する最終パスとして生成する。
|
|
10700
|
+
* connectedCallback 内の inline 生成は「その時点の DOM」しか見えず、router が
|
|
10701
|
+
* 後から挿入するルート内容の構造テンプレートを取り逃がすレースがあった
|
|
10702
|
+
* (state のロード方式と文書順に依存)。renderToString が全要素の完了と
|
|
10703
|
+
* バインディング構築の後にこれを呼ぶことで、スナップショットは常に確定後の
|
|
10704
|
+
* DOM を見る。
|
|
10705
|
+
*
|
|
10706
|
+
* 複数 `enable-ssr` state の意味論は inline 生成と同一に保つ(文書順に生成・
|
|
10707
|
+
* fragment レジストリはモジュール共有・props store は生成ごとにクリア)。
|
|
10708
|
+
* その整理は本プロトコルの範囲外の既存挙動として引き継ぐ。
|
|
10709
|
+
*/
|
|
10710
|
+
function buildSsrDocument(root) {
|
|
10711
|
+
const stateTag = config.tagNames.state;
|
|
10712
|
+
const ssrTag = config.tagNames.ssr;
|
|
10713
|
+
const stateElements = root.querySelectorAll(`${stateTag}[enable-ssr]`);
|
|
10714
|
+
for (const stateEl of stateElements) {
|
|
10715
|
+
const name = stateEl.getAttribute("name") || "default";
|
|
10716
|
+
// 既に直前へ生成済み(旧 server との組み合わせで inline 生成された等)なら
|
|
10717
|
+
// 何もしない — build() は冪等でなければならない(プロトコル契約)
|
|
10718
|
+
const prev = stateEl.previousElementSibling;
|
|
10719
|
+
if (prev !== null &&
|
|
10720
|
+
prev.tagName.toLowerCase() === ssrTag &&
|
|
10721
|
+
(prev.getAttribute("name") || "default") === name) {
|
|
10722
|
+
continue;
|
|
10723
|
+
}
|
|
10724
|
+
const ssrEl = document.createElement(ssrTag);
|
|
10725
|
+
ssrEl.setAttribute("name", name);
|
|
10726
|
+
ssrEl.setAttribute("version", VERSION);
|
|
10727
|
+
Ssr.buildContent(ssrEl, Ssr.extractStateData(stateEl));
|
|
10728
|
+
stateEl.parentNode?.insertBefore(ssrEl, stateEl);
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
const builder = {
|
|
10732
|
+
protocol: "wcs-ssr-snapshot",
|
|
10733
|
+
version: 1,
|
|
10734
|
+
build: buildSsrDocument,
|
|
10735
|
+
};
|
|
10736
|
+
/**
|
|
10737
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
10738
|
+
* binder(registerBinder)と同じ規範 — 既に別のコピーが載っているなら譲る
|
|
10739
|
+
* (そのコピーのレジストリが、そのページの正本だからである)。
|
|
10740
|
+
*/
|
|
10741
|
+
function registerSsrSnapshotBuilder() {
|
|
10742
|
+
const globals = globalThis;
|
|
10743
|
+
if (globals[SSR_SNAPSHOT_BUILDER_KEY] === undefined) {
|
|
10744
|
+
globals[SSR_SNAPSHOT_BUILDER_KEY] = builder;
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
|
|
9884
10748
|
const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
|
|
9885
10749
|
/**
|
|
9886
10750
|
* インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
|
|
@@ -9904,9 +10768,19 @@ function describeImportFailure(name, error, cspBlocked) {
|
|
|
9904
10768
|
return `Failed to evaluate the inline <script> of state "${name}": ${detail}. ` +
|
|
9905
10769
|
`If this page sets a Content-Security-Policy, see ${CSP_GUIDE}`;
|
|
9906
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;
|
|
9907
10781
|
async function loadFromInnerScript(script, name) {
|
|
9908
10782
|
let scriptModule = null;
|
|
9909
|
-
const uniq_comment = `\n//# sourceURL=${name}\n`;
|
|
10783
|
+
const uniq_comment = `\n//# sourceURL=${name}#${++loadSequence}\n`;
|
|
9910
10784
|
// import() が失敗した理由が CSP かどうかを判別するために、評価の間だけ違反を購読する。
|
|
9911
10785
|
let cspBlocked = false;
|
|
9912
10786
|
const onViolation = (event) => {
|
|
@@ -9961,9 +10835,26 @@ async function loadFromJsonFile(url) {
|
|
|
9961
10835
|
}
|
|
9962
10836
|
}
|
|
9963
10837
|
|
|
10838
|
+
/**
|
|
10839
|
+
* `src` の値を **document の base URL** に対して解決する。
|
|
10840
|
+
*
|
|
10841
|
+
* `import(url)` の相対解決は「import を書いたモジュール」を基準にする。ここは
|
|
10842
|
+
* `@wcstack/state` の中なので、素の `import(url)` は `<wcs-state src>` を
|
|
10843
|
+
* **state パッケージの所在**から解決してしまう。同一オリジンに置いたページでは
|
|
10844
|
+
* たまたま一致して見えるが、CDN 一発(`https://esm.run/@wcstack/state/auto`)で
|
|
10845
|
+
* 読み込んだ瞬間に `src="/app.js"` が CDN 側の URL を指して 404 になる。
|
|
10846
|
+
*
|
|
10847
|
+
* `src` は HTML 属性なので、正しい基準は document の base URL である
|
|
10848
|
+
* (`src="*.json"` 側は `fetch` がそう解決しており、同じ属性が形式によって
|
|
10849
|
+
* 違う基準で解決されていた)。絶対 URL・`data:`・`blob:` は URL 解決で
|
|
10850
|
+
* そのまま素通りするため、既存の使い方は影響を受けない。
|
|
10851
|
+
*/
|
|
10852
|
+
function resolveAgainstDocument(url) {
|
|
10853
|
+
return new URL(url, document.baseURI).href;
|
|
10854
|
+
}
|
|
9964
10855
|
async function loadFromScriptFile(url) {
|
|
9965
10856
|
try {
|
|
9966
|
-
const module = await import(/* @vite-ignore */ url);
|
|
10857
|
+
const module = await import(/* @vite-ignore */ resolveAgainstDocument(url));
|
|
9967
10858
|
return module.default || {};
|
|
9968
10859
|
}
|
|
9969
10860
|
catch (e) {
|
|
@@ -12176,6 +13067,41 @@ function disconnectedCallback(target, _prop, receiver, _handler) {
|
|
|
12176
13067
|
}
|
|
12177
13068
|
}
|
|
12178
13069
|
|
|
13070
|
+
/**
|
|
13071
|
+
* getContextListIndex.ts
|
|
13072
|
+
*
|
|
13073
|
+
* Stateの内部APIとして、現在のプロパティ参照スコープにおける
|
|
13074
|
+
* 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
|
|
13075
|
+
* リストインデックス(IListIndex)を取得する関数です。
|
|
13076
|
+
*
|
|
13077
|
+
* 主な役割:
|
|
13078
|
+
* - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
|
|
13079
|
+
* - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
|
|
13080
|
+
*
|
|
13081
|
+
* 設計ポイント:
|
|
13082
|
+
* - 直近のプロパティ参照情報を取得
|
|
13083
|
+
* - info.indexByWildcardPathからstructuredPathのインデックスを特定
|
|
13084
|
+
* - listIndex.at(index)で該当階層のリストインデックスを取得
|
|
13085
|
+
* - パスが一致しない場合や参照が存在しない場合はnullを返す
|
|
13086
|
+
*/
|
|
13087
|
+
function getContextListIndex(handler, structuredPath) {
|
|
13088
|
+
if (handler.addressStackLength === 0) {
|
|
13089
|
+
return null;
|
|
13090
|
+
}
|
|
13091
|
+
const address = handler.lastAddressStack;
|
|
13092
|
+
if (address === null) {
|
|
13093
|
+
return null;
|
|
13094
|
+
}
|
|
13095
|
+
const index = address.pathInfo.indexByWildcardPath[structuredPath];
|
|
13096
|
+
if (typeof index === "undefined") {
|
|
13097
|
+
return null;
|
|
13098
|
+
}
|
|
13099
|
+
if (address.listIndex === null) {
|
|
13100
|
+
return null;
|
|
13101
|
+
}
|
|
13102
|
+
return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
|
|
13103
|
+
}
|
|
13104
|
+
|
|
12179
13105
|
const cacheEntryByAbsoluteStateAddress = new WeakMap();
|
|
12180
13106
|
function getCacheEntryByAbsoluteStateAddress(address) {
|
|
12181
13107
|
return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
|
|
@@ -12463,38 +13389,112 @@ function getByAddress(target, address, receiver, handler) {
|
|
|
12463
13389
|
}
|
|
12464
13390
|
|
|
12465
13391
|
/**
|
|
12466
|
-
*
|
|
13392
|
+
* wildcardIndexes.ts
|
|
12467
13393
|
*
|
|
12468
|
-
*
|
|
12469
|
-
*
|
|
12470
|
-
*
|
|
13394
|
+
* ワイルドカードを含むパスから「解決済み添字タプルの集合」を列挙する共有走査。
|
|
13395
|
+
* `$getAll`(読み)と `$setAll`(書き)が**同じ展開規則・同じ順序**で動くための単一の正本
|
|
13396
|
+
* (docs/state-set-all-design.md §6-1)。
|
|
12471
13397
|
*
|
|
12472
|
-
*
|
|
12473
|
-
*
|
|
12474
|
-
*
|
|
13398
|
+
* 添字は**前方一致の接頭辞**で、足りない分は「その階層を全部展開する」という意味を持つ
|
|
13399
|
+
* (README の `$getAll("scores.*", [])` がこれ)。返るタプルは常にワイルドカードの本数と
|
|
13400
|
+
* 同じ長さになるので、そのまま `$resolve` の厳密一致な添字として使える。
|
|
12475
13401
|
*
|
|
12476
|
-
*
|
|
12477
|
-
*
|
|
12478
|
-
*
|
|
12479
|
-
*
|
|
12480
|
-
* -
|
|
13402
|
+
* 順序は**深さ優先・添字昇順**(ネストは添字タプルの辞書順)で決定的。
|
|
13403
|
+
* `$getAll(p, i)` の戻り順と `$setAll(p, i, …)` の適用順が一致する根拠がこれであり、
|
|
13404
|
+
* `$setAll` の `{ spread: true }` 形はこの順序に乗っている。
|
|
13405
|
+
*
|
|
13406
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
|
|
12481
13407
|
*/
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
13408
|
+
/**
|
|
13409
|
+
* 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
|
|
13410
|
+
* ListIndex の同一性を跨いで保つために使う。
|
|
13411
|
+
*
|
|
13412
|
+
* 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
|
|
13413
|
+
* 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
|
|
13414
|
+
*/
|
|
13415
|
+
// ToDo: IAbsoluteStateAddressに変更する
|
|
13416
|
+
const lastValueByListAddress = new WeakMap();
|
|
13417
|
+
/**
|
|
13418
|
+
* `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
|
|
13419
|
+
* マッチする添字タプルを列挙する。
|
|
13420
|
+
*
|
|
13421
|
+
* 添字の本数検査(上限)は呼び出し側の責務 — API 名を診断メッセージに出すため。
|
|
13422
|
+
*/
|
|
13423
|
+
function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, options) {
|
|
13424
|
+
const newValueByAddress = new Map();
|
|
13425
|
+
const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
|
|
13426
|
+
const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
|
|
13427
|
+
if (wildcardParentPathInfo === null) {
|
|
13428
|
+
results.push(parentIndexes);
|
|
13429
|
+
return;
|
|
13430
|
+
}
|
|
13431
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13432
|
+
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13433
|
+
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13434
|
+
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13435
|
+
const listIndexes = listDiff.newIndexes;
|
|
13436
|
+
const index = indexes[indexPos] ?? null;
|
|
13437
|
+
newValueByAddress.set(wildcardAddress, newValue);
|
|
13438
|
+
if (index === null) {
|
|
13439
|
+
for (let i = 0; i < listIndexes.length; i++) {
|
|
13440
|
+
const listIndex = listIndexes[i];
|
|
13441
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13442
|
+
}
|
|
13443
|
+
}
|
|
13444
|
+
else {
|
|
13445
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13446
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13447
|
+
const listIndex = listIndexes[index] ??
|
|
13448
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13449
|
+
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13450
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13451
|
+
}
|
|
13452
|
+
else {
|
|
13453
|
+
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13454
|
+
results.push(parentIndexes.concat(listIndex.index));
|
|
13455
|
+
}
|
|
13456
|
+
}
|
|
13457
|
+
};
|
|
13458
|
+
const resultIndexes = [];
|
|
13459
|
+
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
13460
|
+
if (options.commitDiffBaseline) {
|
|
13461
|
+
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13462
|
+
lastValueByListAddress.set(address, newValue);
|
|
13463
|
+
}
|
|
12496
13464
|
}
|
|
12497
|
-
return
|
|
13465
|
+
return resultIndexes;
|
|
13466
|
+
}
|
|
13467
|
+
|
|
13468
|
+
/**
|
|
13469
|
+
* getListIndexByIndexes.ts
|
|
13470
|
+
*
|
|
13471
|
+
* 解決済みの添字タプル(ワイルドカード 1 段につき 1 個)から、対応する ListIndex を
|
|
13472
|
+
* **正本レジストリ**(listIndexesByList)経由で引き当てる。
|
|
13473
|
+
*
|
|
13474
|
+
* `$resolve` と `$setAll` の共有部分。列挙側(wildcardIndexes.ts)が走査中に生成した
|
|
13475
|
+
* ListIndex をそのまま書き込み先にせず、ここで引き直すことで、binding が使っている
|
|
13476
|
+
* ListIndex と同一の同一性に載る(docs/state-set-all-design.md §6-2)。
|
|
13477
|
+
*
|
|
13478
|
+
* 添字の本数がワイルドカードの本数と一致していることは呼び出し側の責務。
|
|
13479
|
+
*/
|
|
13480
|
+
function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
|
|
13481
|
+
// ワイルドカード階層ごとにListIndexを解決していく
|
|
13482
|
+
let listIndex = null;
|
|
13483
|
+
for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
|
|
13484
|
+
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13485
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13486
|
+
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13487
|
+
const listIndexes = getListIndexesByList(tmpValue);
|
|
13488
|
+
if (listIndexes == null) {
|
|
13489
|
+
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13490
|
+
}
|
|
13491
|
+
const index = indexes[i];
|
|
13492
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13493
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13494
|
+
listIndex = listIndexes[index] ??
|
|
13495
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13496
|
+
}
|
|
13497
|
+
return listIndex;
|
|
12498
13498
|
}
|
|
12499
13499
|
|
|
12500
13500
|
/**
|
|
@@ -13500,22 +14500,8 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13500
14500
|
if (indexes.length !== pathInfo.wildcardParentPathInfos.length) {
|
|
13501
14501
|
raiseError(indexArityMessage("$resolve", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
13502
14502
|
}
|
|
13503
|
-
// ワイルドカード階層ごとにListIndex
|
|
13504
|
-
|
|
13505
|
-
for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
|
|
13506
|
-
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13507
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13508
|
-
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13509
|
-
const listIndexes = getListIndexesByList(tmpValue);
|
|
13510
|
-
if (listIndexes == null) {
|
|
13511
|
-
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13512
|
-
}
|
|
13513
|
-
const index = indexes[i];
|
|
13514
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13515
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13516
|
-
listIndex = listIndexes[index] ??
|
|
13517
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13518
|
-
}
|
|
14503
|
+
// ワイルドカード階層ごとにListIndexを解決していく(`$setAll` と共有)
|
|
14504
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, indexes);
|
|
13519
14505
|
// ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
|
|
13520
14506
|
const address = createStateAddress(pathInfo, listIndex);
|
|
13521
14507
|
const hasSetValue = typeof value !== "undefined";
|
|
@@ -13532,14 +14518,18 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13532
14518
|
* getAllReadonly
|
|
13533
14519
|
*
|
|
13534
14520
|
* ワイルドカードを含む State パスから、対象となる全要素を配列で取得する。
|
|
13535
|
-
*
|
|
14521
|
+
* 走査そのものは `$setAll` と共有する(wildcardIndexes.ts)。
|
|
14522
|
+
*
|
|
14523
|
+
* `indexes` 省略時の既定はループ文脈の添字 `[$1..$n]`。正確には「path と文脈が
|
|
14524
|
+
* 共有するワイルドカード連鎖の分だけ文脈の添字を接頭辞として敷く」(整合最長接頭辞)。
|
|
14525
|
+
* 共有が無いのに文脈が添字を持つ場合は throw する — 異なる文脈の添字は流用しない。
|
|
14526
|
+
*
|
|
14527
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)、
|
|
14528
|
+
* 添字本数超過(wcs/index-arity)、省略時の文脈不整合(getAllContextMismatchMessage)
|
|
13536
14529
|
*/
|
|
13537
|
-
// ToDo: IAbsoluteStateAddressに変更する
|
|
13538
|
-
const lastValueByListAddress = new WeakMap();
|
|
13539
14530
|
function getAll(target, prop, receiver, handler) {
|
|
13540
14531
|
const resolveFn = resolve(target, prop, receiver, handler);
|
|
13541
14532
|
return (path, indexes) => {
|
|
13542
|
-
const newValueByAddress = new Map();
|
|
13543
14533
|
const pathInfo = getPathInfo(path);
|
|
13544
14534
|
if (handler.addressStackLength > 0) {
|
|
13545
14535
|
const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
|
|
@@ -13560,60 +14550,39 @@ function getAll(target, prop, receiver, handler) {
|
|
|
13560
14550
|
raiseError(indexArityMessage("$getAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
13561
14551
|
}
|
|
13562
14552
|
if (typeof indexes === "undefined") {
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
14553
|
+
// 省略時の既定はループ文脈の添字 `[$1..$n]`。ただし敷けるのは path と文脈が
|
|
14554
|
+
// **共有するワイルドカード連鎖**の分だけなので、path のワイルドカードを
|
|
14555
|
+
// 内側(最深)から探し、最初に文脈にヒットした階層の scoped indexes を接頭辞にする。
|
|
14556
|
+
// ワイルドカードパスの序数はパス文字列自身の `*` の本数で決まるため、深い側が
|
|
14557
|
+
// ヒットすれば浅い側は必ず含まれ、これが整合する最長の接頭辞になる。文脈が
|
|
14558
|
+
// path より深い分は自然に切り詰められ、導出した接頭辞は path のワイルドカード
|
|
14559
|
+
// 本数を超えないので、上の本数検査には掛けない。
|
|
14560
|
+
for (let i = pathInfo.wildcardPaths.length - 1; i >= 0; i--) {
|
|
14561
|
+
const listIndex = getContextListIndex(handler, pathInfo.wildcardPaths[i]);
|
|
13566
14562
|
if (listIndex) {
|
|
13567
14563
|
indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
|
|
13568
14564
|
break;
|
|
13569
14565
|
}
|
|
13570
14566
|
}
|
|
13571
14567
|
if (typeof indexes === "undefined") {
|
|
14568
|
+
// 共有ゼロ。文脈が自スコープの添字を実際に持っているなら、既定の `[...$n]` は
|
|
14569
|
+
// **異なる文脈の添字の流用(混入)**になるため、黙って全展開へ倒さず throw する。
|
|
14570
|
+
// 文脈そのものが無い(トップレベル getter / メソッド直下)なら全展開が既定。
|
|
14571
|
+
const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
|
|
14572
|
+
const contextListIndex = lastAddress?.listIndex ?? null;
|
|
14573
|
+
if (pathInfo.wildcardCount > 0 && lastAddress !== null && contextListIndex !== null &&
|
|
14574
|
+
contextListIndex.length - getBaseDepth(handler.stateElement) > 0) {
|
|
14575
|
+
raiseError(getAllContextMismatchMessage(path, lastAddress.pathInfo.path));
|
|
14576
|
+
}
|
|
13572
14577
|
indexes = [];
|
|
13573
14578
|
}
|
|
13574
14579
|
}
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
if (wildcardParentPathInfo === null) {
|
|
13578
|
-
results.push(parentIndexes);
|
|
13579
|
-
return;
|
|
13580
|
-
}
|
|
13581
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13582
|
-
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13583
|
-
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13584
|
-
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13585
|
-
const listIndexes = listDiff.newIndexes;
|
|
13586
|
-
const index = indexes[indexPos] ?? null;
|
|
13587
|
-
newValueByAddress.set(wildcardAddress, newValue);
|
|
13588
|
-
if (index === null) {
|
|
13589
|
-
for (let i = 0; i < listIndexes.length; i++) {
|
|
13590
|
-
const listIndex = listIndexes[i];
|
|
13591
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13592
|
-
}
|
|
13593
|
-
}
|
|
13594
|
-
else {
|
|
13595
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13596
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13597
|
-
const listIndex = listIndexes[index] ??
|
|
13598
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13599
|
-
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13600
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13601
|
-
}
|
|
13602
|
-
else {
|
|
13603
|
-
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13604
|
-
results.push(parentIndexes.concat(listIndex.index));
|
|
13605
|
-
}
|
|
13606
|
-
}
|
|
13607
|
-
};
|
|
13608
|
-
const resultIndexes = [];
|
|
13609
|
-
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
14580
|
+
// 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
|
|
14581
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
|
|
13610
14582
|
const resultValues = [];
|
|
13611
14583
|
for (let i = 0; i < resultIndexes.length; i++) {
|
|
13612
14584
|
resultValues.push(resolveFn(pathInfo.path, resultIndexes[i]));
|
|
13613
14585
|
}
|
|
13614
|
-
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13615
|
-
lastValueByListAddress.set(address, newValue);
|
|
13616
|
-
}
|
|
13617
14586
|
return resultValues;
|
|
13618
14587
|
};
|
|
13619
14588
|
}
|
|
@@ -13697,6 +14666,83 @@ function postUpdate(target, _prop, receiver, handler) {
|
|
|
13697
14666
|
};
|
|
13698
14667
|
}
|
|
13699
14668
|
|
|
14669
|
+
/**
|
|
14670
|
+
* setAll.ts
|
|
14671
|
+
*
|
|
14672
|
+
* ワイルドカードを含む State パスにマッチする**全アドレスへ一括で書き込む**。
|
|
14673
|
+
* `$getAll`(読み)の対称形(docs/state-set-all-design.md)。
|
|
14674
|
+
*
|
|
14675
|
+
* 存在理由は糖衣ではなく「**リスト全置換の回避**」(設計 §1-1)。
|
|
14676
|
+
* `this.users = this.users.map(...)` は配列を作り直すので ListIndex・行 getter
|
|
14677
|
+
* キャッシュ・差分描画がまとめて作り直しになる。`$setAll` は意味としては一括更新、
|
|
14678
|
+
* 実体は in-place な個別書き込みで、同じことを差分に載せたまま行う。
|
|
14679
|
+
*
|
|
14680
|
+
* 3 つの形(設計 §2):
|
|
14681
|
+
* - ブロードキャスト `$setAll(path, indexes, value)`
|
|
14682
|
+
* - mapper(第一級) `$setAll(path, indexes, (current, ...indexes) => next)`
|
|
14683
|
+
* - spread `$setAll(path, indexes, values, { spread: true })`
|
|
14684
|
+
*/
|
|
14685
|
+
function setAll(target, _prop, receiver, handler) {
|
|
14686
|
+
return (path, indexes, value, options) => {
|
|
14687
|
+
const pathInfo = getPathInfo(path);
|
|
14688
|
+
// 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
|
|
14689
|
+
// 「現在行」ではなく「全行」を意味する(設計 §4-1)。
|
|
14690
|
+
if (!Array.isArray(indexes)) {
|
|
14691
|
+
raiseError(setAllValueKindMessage(path, "requires an explicit indexes array (pass [] to expand every level)."));
|
|
14692
|
+
}
|
|
14693
|
+
// 添字は前方一致の接頭辞なので不足は正当。超過だけを弾く(`$getAll` と同じ規則)。
|
|
14694
|
+
if (indexes.length > pathInfo.wildcardParentPathInfos.length) {
|
|
14695
|
+
raiseError(indexArityMessage("$setAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
14696
|
+
}
|
|
14697
|
+
const spread = options?.spread === true;
|
|
14698
|
+
const isMapper = typeof value === "function";
|
|
14699
|
+
if (spread && isMapper) {
|
|
14700
|
+
raiseError(setAllValueKindMessage(path, "cannot combine { spread: true } with a mapper function."));
|
|
14701
|
+
}
|
|
14702
|
+
if (spread && !Array.isArray(value)) {
|
|
14703
|
+
raiseError(setAllValueKindMessage(path, "requires an array as the value when { spread: true } is set."));
|
|
14704
|
+
}
|
|
14705
|
+
// --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
|
|
14706
|
+
// 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
|
|
14707
|
+
// 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
|
|
14708
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
|
|
14709
|
+
if (spread && value.length !== resultIndexes.length) {
|
|
14710
|
+
raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
|
|
14711
|
+
}
|
|
14712
|
+
const addresses = [];
|
|
14713
|
+
for (let i = 0; i < resultIndexes.length; i++) {
|
|
14714
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, resultIndexes[i]);
|
|
14715
|
+
addresses.push(createStateAddress(pathInfo, listIndex));
|
|
14716
|
+
}
|
|
14717
|
+
// --- 第 2 相: 確定したアドレスにだけ書く ---
|
|
14718
|
+
let written = 0;
|
|
14719
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
14720
|
+
const address = addresses[i];
|
|
14721
|
+
let nextValue;
|
|
14722
|
+
if (isMapper) {
|
|
14723
|
+
// 現在値は書く直前に読む。先行する書き込みが getter 経由で他行に及ぶ場合、
|
|
14724
|
+
// mapper が見るべきなのは最新値。
|
|
14725
|
+
const currentValue = getByAddress(target, address, receiver, handler);
|
|
14726
|
+
nextValue = value(currentValue, ...resultIndexes[i]);
|
|
14727
|
+
}
|
|
14728
|
+
else if (spread) {
|
|
14729
|
+
nextValue = value[i];
|
|
14730
|
+
}
|
|
14731
|
+
else {
|
|
14732
|
+
nextValue = value;
|
|
14733
|
+
}
|
|
14734
|
+
// undefined は常にスキップ(設計 §5)。mapper の return 忘れで全行を潰さないため、
|
|
14735
|
+
// かつ「この行は変えない」を表現できるようにするため。クリアは null。
|
|
14736
|
+
if (typeof nextValue === "undefined") {
|
|
14737
|
+
continue;
|
|
14738
|
+
}
|
|
14739
|
+
setByAddress(target, address, nextValue, receiver, handler);
|
|
14740
|
+
written++;
|
|
14741
|
+
}
|
|
14742
|
+
return written;
|
|
14743
|
+
};
|
|
14744
|
+
}
|
|
14745
|
+
|
|
13700
14746
|
/**
|
|
13701
14747
|
* trackDependency.ts
|
|
13702
14748
|
*
|
|
@@ -13871,7 +14917,7 @@ function setLoopContext(handler, loopContext, callback) {
|
|
|
13871
14917
|
* StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
|
|
13872
14918
|
*
|
|
13873
14919
|
* 主な役割:
|
|
13874
|
-
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
|
|
14920
|
+
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $setAll, $postUpdate,
|
|
13875
14921
|
* $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
|
|
13876
14922
|
* - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
|
|
13877
14923
|
* - getByRefで構造化パス・リストインデックスに対応した値を取得
|
|
@@ -13934,6 +14980,11 @@ function get(target, prop, receiver, handler) {
|
|
|
13934
14980
|
return getAll(target, prop, receiver, handler)(path, indexes);
|
|
13935
14981
|
};
|
|
13936
14982
|
}
|
|
14983
|
+
case "$setAll": {
|
|
14984
|
+
return (path, indexes, value, options) => {
|
|
14985
|
+
return setAll(target, prop, receiver, handler)(path, indexes, value, options);
|
|
14986
|
+
};
|
|
14987
|
+
}
|
|
13937
14988
|
case "$postUpdate": {
|
|
13938
14989
|
return (path) => {
|
|
13939
14990
|
return postUpdate(target, prop, receiver, handler)(path);
|
|
@@ -14204,9 +15255,58 @@ function meltFrozenObject(frozenObj) {
|
|
|
14204
15255
|
class InnerStateProxyHandler {
|
|
14205
15256
|
_webComponent;
|
|
14206
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;
|
|
14207
15270
|
constructor(webComponent, stateName) {
|
|
14208
15271
|
this._webComponent = webComponent;
|
|
14209
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);
|
|
14210
15310
|
}
|
|
14211
15311
|
/**
|
|
14212
15312
|
* 親スコープで読み書きするときのループ文脈を決める。候補は 2 つある。
|
|
@@ -14259,6 +15359,10 @@ class InnerStateProxyHandler {
|
|
|
14259
15359
|
if (this._innerStateElement.getterPaths.has(prop) && prop in target) {
|
|
14260
15360
|
return Reflect.get(target, prop, receiver);
|
|
14261
15361
|
}
|
|
15362
|
+
// 1'. R1: ルートマウント下の own data key → 私有(マッピングより先)
|
|
15363
|
+
if (this._isPrivateKey(target, prop)) {
|
|
15364
|
+
return Reflect.get(target, prop, receiver);
|
|
15365
|
+
}
|
|
14262
15366
|
// 2 & 3. マッピング完全一致 / サブパス → 親の状態
|
|
14263
15367
|
const innerPathInfo = getPathInfo(prop);
|
|
14264
15368
|
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
@@ -14290,6 +15394,10 @@ class InnerStateProxyHandler {
|
|
|
14290
15394
|
if (this._innerStateElement.setterPaths.has(prop) && prop in target) {
|
|
14291
15395
|
return Reflect.set(target, prop, value, receiver);
|
|
14292
15396
|
}
|
|
15397
|
+
// 1'. R1: ルートマウント下の own data key → 私有に書く
|
|
15398
|
+
if (this._isPrivateKey(target, prop)) {
|
|
15399
|
+
return Reflect.set(target, prop, value, receiver);
|
|
15400
|
+
}
|
|
14293
15401
|
// 2 & 3. マッピング完全一致 / サブパス → 親に書く
|
|
14294
15402
|
const innerPathInfo = getPathInfo(prop);
|
|
14295
15403
|
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
@@ -14323,6 +15431,11 @@ class InnerStateProxyHandler {
|
|
|
14323
15431
|
if ((this._innerStateElement.getterPaths.has(prop) || this._innerStateElement.setterPaths.has(prop)) && prop in target) {
|
|
14324
15432
|
return true;
|
|
14325
15433
|
}
|
|
15434
|
+
// 1'. R1: 先頭が私有キーなら素のオブジェクトの答え(完全一致だけ真。`draft.title` は
|
|
15435
|
+
// 偽を返して getByAddress に親アドレスから降りてもらう)
|
|
15436
|
+
if (this._isPrivateKey(target, prop)) {
|
|
15437
|
+
return prop in target;
|
|
15438
|
+
}
|
|
14326
15439
|
// 2 & 3. マッピング
|
|
14327
15440
|
const innerPathInfo = getPathInfo(prop);
|
|
14328
15441
|
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
@@ -14414,6 +15527,116 @@ function createOuterState(webComponent, stateName) {
|
|
|
14414
15527
|
return new Proxy({}, handler);
|
|
14415
15528
|
}
|
|
14416
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
|
+
|
|
14417
15640
|
const getOuter = (outerState) => () => outerState;
|
|
14418
15641
|
function bindWebComponent(innerStateElement, component, stateProp, state) {
|
|
14419
15642
|
setStateElementByWebComponent(component, stateProp, innerStateElement);
|
|
@@ -14430,6 +15653,9 @@ function bindWebComponent(innerStateElement, component, stateProp, state) {
|
|
|
14430
15653
|
// innerState proxy、plain なら melt 済みのローカル state。
|
|
14431
15654
|
if (bindings.length > 0) {
|
|
14432
15655
|
buildPrimaryMappingRule(component, stateProp, bindings);
|
|
15656
|
+
// own data key とマウントの衝突を 1 回だけ報告する(R1 の私有キーがツリーを隠す形と、
|
|
15657
|
+
// 部分マウントで v2 に反転する形 — docs/state-mount-design.md D19)。
|
|
15658
|
+
warnOwnKeyShadows(component, stateProp, state);
|
|
14433
15659
|
// 値の正本が親スコープにあることを state 要素に記録する。越境アドレスの受け渡しと
|
|
14434
15660
|
// リストパスの外向き伝播はこのフラグでのみ有効になる(§1.8)。
|
|
14435
15661
|
innerStateElement.markComponentStateMapped?.();
|
|
@@ -14460,6 +15686,27 @@ function bindWebComponent(innerStateElement, component, stateProp, state) {
|
|
|
14460
15686
|
}
|
|
14461
15687
|
}
|
|
14462
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
|
+
|
|
14463
15710
|
function getStateInfo(state) {
|
|
14464
15711
|
const getterPaths = new Set();
|
|
14465
15712
|
const setterPaths = new Set();
|
|
@@ -14690,6 +15937,11 @@ class State extends HTMLElementBase {
|
|
|
14690
15937
|
}
|
|
14691
15938
|
await this._loadingPromise;
|
|
14692
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
|
+
}
|
|
14693
15945
|
setStateElementByName(this.rootNode, this._name, this);
|
|
14694
15946
|
}
|
|
14695
15947
|
async _initializeBindWebComponent() {
|
|
@@ -14725,6 +15977,11 @@ class State extends HTMLElementBase {
|
|
|
14725
15977
|
raiseError(`"bind-component" cannot be combined with ${conflicting.join(", ")}. The component's "${this.getAttribute("bind-component")}" property is the only state source.`);
|
|
14726
15978
|
}
|
|
14727
15979
|
const boundComponentStateProp = this.getAttribute("bind-component");
|
|
15980
|
+
// 束ねる意思をここで宣言する(完了はずっと後)。丸ごとマウント `state: user` の
|
|
15981
|
+
// 完了前の初期適用は、この宣言を見て書き込みを抑止する
|
|
15982
|
+
// (webComponent/completeWebComponent.ts)。下の await より前でなければ、
|
|
15983
|
+
// 親の初期適用が先に走って親のオブジェクトを state プロパティに書いてしまう。
|
|
15984
|
+
markWebComponentStatePropDeclared(boundComponent, boundComponentStateProp);
|
|
14728
15985
|
const componentRegistry = getCustomElementRegistry(boundComponent);
|
|
14729
15986
|
if (componentRegistry === null) {
|
|
14730
15987
|
// null レジストリのサブツリーではホストは永久に upgrade されない。
|
|
@@ -14739,10 +15996,19 @@ class State extends HTMLElementBase {
|
|
|
14739
15996
|
if (!(boundComponentStateProp in boundComponent)) {
|
|
14740
15997
|
raiseError(`Component does not have property "${boundComponentStateProp}" for state binding.`);
|
|
14741
15998
|
}
|
|
14742
|
-
|
|
15999
|
+
let state = boundComponent[boundComponentStateProp];
|
|
14743
16000
|
if (typeof state !== 'object' || state === null) {
|
|
14744
16001
|
raiseError(`Component property "${boundComponentStateProp}" is not an object for state binding.`);
|
|
14745
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
|
+
}
|
|
14746
16012
|
this._boundComponent = boundComponent;
|
|
14747
16013
|
this._boundComponentStateProp = boundComponentStateProp;
|
|
14748
16014
|
bindWebComponent(this, this._boundComponent, this._boundComponentStateProp, state);
|
|
@@ -14799,11 +16065,17 @@ class State extends HTMLElementBase {
|
|
|
14799
16065
|
}
|
|
14800
16066
|
// mapped = プライマリ規則が 1 件以上あることと同義(bindWebComponent の分岐)
|
|
14801
16067
|
const innerPaths = getPrimaryInnerPaths(this._boundComponent);
|
|
16068
|
+
// ルート規則(丸ごとマウント)は内側パスが空なので、子の登録済みパス全部を読み直す
|
|
16069
|
+
// (webComponent/rootReloadPaths.ts)
|
|
16070
|
+
const rootPaths = hasRootMappingRule(this._boundComponent) ? getRootReloadPaths(this) : [];
|
|
14802
16071
|
resetDerivedMappingRules(this._boundComponent);
|
|
14803
16072
|
this.createState("readonly", (state) => {
|
|
14804
16073
|
for (const path of innerPaths) {
|
|
14805
16074
|
state.$postUpdate(path);
|
|
14806
16075
|
}
|
|
16076
|
+
for (const path of rootPaths) {
|
|
16077
|
+
state.$postUpdate(path);
|
|
16078
|
+
}
|
|
14807
16079
|
});
|
|
14808
16080
|
}
|
|
14809
16081
|
async _callStateConnectedCallback() {
|
|
@@ -14896,8 +16168,12 @@ class State extends HTMLElementBase {
|
|
|
14896
16168
|
if (!this.hasAttribute('enable-ssr') || inSsr()) {
|
|
14897
16169
|
await this._callStateConnectedCallback();
|
|
14898
16170
|
}
|
|
14899
|
-
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr>
|
|
14900
|
-
|
|
16171
|
+
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
|
|
16172
|
+
// orchestrated(サーバー主導の最終パス、docs/ssr-router-design.md §5)では
|
|
16173
|
+
// 生成しない — renderToString が全要素の完了後にまとめて生成するため。
|
|
16174
|
+
// ここで生成すると、router 等が後から挿入した内容の構造テンプレートを
|
|
16175
|
+
// 取り逃がすレースがある(state のロード方式と文書順に依存)
|
|
16176
|
+
if (inSsr() && this.hasAttribute('enable-ssr') && !isOrchestratedSsr()) {
|
|
14901
16177
|
try {
|
|
14902
16178
|
await getBindingsReady(this.rootNode);
|
|
14903
16179
|
const name = this.getAttribute('name') || 'default';
|
|
@@ -15044,6 +16320,9 @@ class State extends HTMLElementBase {
|
|
|
15044
16320
|
get hasMappedComponentState() {
|
|
15045
16321
|
return this._hasMappedComponentState;
|
|
15046
16322
|
}
|
|
16323
|
+
get boundPaths() {
|
|
16324
|
+
return this._pathSet;
|
|
16325
|
+
}
|
|
15047
16326
|
/**
|
|
15048
16327
|
* この state の実体が innerState proxy であることを記録する。唯一の呼び手は
|
|
15049
16328
|
* `bindWebComponent` の mapped 分岐(§1.8)。
|
|
@@ -15199,11 +16478,63 @@ function registerComponents(registry = customElements) {
|
|
|
15199
16478
|
}
|
|
15200
16479
|
}
|
|
15201
16480
|
|
|
16481
|
+
/**
|
|
16482
|
+
* `<html lang>` を既定ロケールとして採る。
|
|
16483
|
+
*
|
|
16484
|
+
* ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は `config.locale`
|
|
16485
|
+
* を読むが、それを設定できる公開の入口は `bootstrapState({ locale })` しかない。
|
|
16486
|
+
* 一方 `auto` エントリは `bootstrapState()` を引数なしで呼ぶため、CDN 一発
|
|
16487
|
+
* (`<script src=".../@wcstack/state/auto">`)で読み込んだページには**ロケールを
|
|
16488
|
+
* 渡す口が無かった**。auto バンドルは SRI のため自己完結で、別途 `@wcstack/state`
|
|
16489
|
+
* を import して `bootstrapState` を呼んでも別インスタンスになり効かない。
|
|
16490
|
+
*
|
|
16491
|
+
* `<html lang>` はページのロケールを書く HTML 標準の場所であり、SSR ではサーバーが、
|
|
16492
|
+
* 静的ページでは head のスニペットが DOM 解析前に書く。そこを既定にすると
|
|
16493
|
+
* **ロケールの正本が 1 つになり**、「設定を早く呼ぶ」という守りにくい順序の約束が
|
|
16494
|
+
* 「`<html lang>` が state のロードより前にある」という構造的な保証に変わる。
|
|
16495
|
+
*
|
|
16496
|
+
* 明示指定(`bootstrapState({ locale })`)が常に優先する。
|
|
16497
|
+
*/
|
|
16498
|
+
function localeFromDocument() {
|
|
16499
|
+
const lang = document.documentElement?.lang;
|
|
16500
|
+
if (!lang) {
|
|
16501
|
+
return undefined;
|
|
16502
|
+
}
|
|
16503
|
+
try {
|
|
16504
|
+
// 妥当な BCP-47 タグでなければ Intl が RangeError を投げる。不正な lang を
|
|
16505
|
+
// そのまま採ると、これまで既定 'en' で動いていたページのフィルタが実行時に
|
|
16506
|
+
// 落ちる。既定へ落として警告するほうが、黙って壊すより回復しやすい。
|
|
16507
|
+
Intl.getCanonicalLocales(lang);
|
|
16508
|
+
return lang;
|
|
16509
|
+
}
|
|
16510
|
+
catch {
|
|
16511
|
+
console.warn(`[@wcstack/state] <html lang="${lang}"> is not a valid BCP-47 language tag. ` +
|
|
16512
|
+
`Falling back to the default locale for filters.`);
|
|
16513
|
+
return undefined;
|
|
16514
|
+
}
|
|
16515
|
+
}
|
|
16516
|
+
function resolveConfig(config) {
|
|
16517
|
+
if (typeof config?.locale === "string") {
|
|
16518
|
+
return config;
|
|
16519
|
+
}
|
|
16520
|
+
const locale = localeFromDocument();
|
|
16521
|
+
if (locale === undefined) {
|
|
16522
|
+
return config;
|
|
16523
|
+
}
|
|
16524
|
+
return { ...config, locale };
|
|
16525
|
+
}
|
|
15202
16526
|
function bootstrapState(config, registry) {
|
|
15203
|
-
|
|
15204
|
-
|
|
16527
|
+
const resolved = resolveConfig(config);
|
|
16528
|
+
if (resolved) {
|
|
16529
|
+
setConfig(resolved);
|
|
15205
16530
|
}
|
|
15206
16531
|
registerComponents(registry);
|
|
16532
|
+
// binder プロトコルの提供(docs/binder-protocol-design.md)。router が後から
|
|
16533
|
+
// 差し込むノードをバインドできるようにする。登録は冪等。
|
|
16534
|
+
registerBinder();
|
|
16535
|
+
// ssr-snapshot プロトコルの提供(docs/ssr-router-design.md §5)。renderToString が
|
|
16536
|
+
// <wcs-ssr> 生成をサーバー主導の最終パスへ回せるようにする。登録は冪等。
|
|
16537
|
+
registerSsrSnapshotBuilder();
|
|
15207
16538
|
// DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
|
|
15208
16539
|
registerDevtoolsSource();
|
|
15209
16540
|
}
|