@wcstack/state 1.30.0 → 1.32.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 +265 -6
- package/README.md +267 -6
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +82 -3
- package/dist/index.esm.js +1441 -192
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +28 -8
- package/dist/parser.esm.js +28 -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 を差し替えてサーバーレンダリング
|
|
@@ -351,14 +408,7 @@ function getCustomElement(node) {
|
|
|
351
408
|
}
|
|
352
409
|
}
|
|
353
410
|
|
|
354
|
-
|
|
355
|
-
* Resolve the registry at operation time so importing the runtime remains safe
|
|
356
|
-
* when browser globals are absent. The owner hook is reserved for scoped
|
|
357
|
-
* registries; current callers fall back to the global registry.
|
|
358
|
-
*/
|
|
359
|
-
function getCustomElementRegistry(owner = null) {
|
|
360
|
-
const globalRegistry = globalThis.customElements;
|
|
361
|
-
const registry = owner?.customElements ?? globalRegistry;
|
|
411
|
+
function toAdapter(registry) {
|
|
362
412
|
if (typeof registry !== "object" || registry === null)
|
|
363
413
|
return null;
|
|
364
414
|
const candidate = registry;
|
|
@@ -367,6 +417,33 @@ function getCustomElementRegistry(owner = null) {
|
|
|
367
417
|
}
|
|
368
418
|
return candidate;
|
|
369
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Resolve the registry that governs `owner` at operation time, so importing the
|
|
422
|
+
* runtime stays safe when browser globals are absent.
|
|
423
|
+
*
|
|
424
|
+
* Pass the node the operation is about (the bound element, its shadow root):
|
|
425
|
+
* with scoped custom element registries the same tag name can resolve to a
|
|
426
|
+
* different constructor per tree, so "is this tag defined?" is only meaningful
|
|
427
|
+
* relative to a node. Nodes on platforms without scoped registries report
|
|
428
|
+
* `undefined` and fall back to the global registry, which keeps every existing
|
|
429
|
+
* caller on today's behaviour.
|
|
430
|
+
*/
|
|
431
|
+
function getCustomElementRegistry(owner = null) {
|
|
432
|
+
if (owner !== null && typeof owner !== "undefined") {
|
|
433
|
+
const { customElementRegistry: scoped, customElements: owned } = owner;
|
|
434
|
+
// A node in a null-registry subtree resolves to no registry at all. Falling
|
|
435
|
+
// back to the global one would report globally-defined tags as usable and
|
|
436
|
+
// let us write own properties onto elements that are still un-upgraded --
|
|
437
|
+
// exactly the accessor shadowing the deferred-apply path exists to avoid.
|
|
438
|
+
if (scoped === null)
|
|
439
|
+
return null;
|
|
440
|
+
if (typeof scoped !== "undefined")
|
|
441
|
+
return toAdapter(scoped);
|
|
442
|
+
if (typeof owned !== "undefined")
|
|
443
|
+
return toAdapter(owned);
|
|
444
|
+
}
|
|
445
|
+
return toAdapter(globalThis.customElements);
|
|
446
|
+
}
|
|
370
447
|
function upgradeCustomElement(registry, root) {
|
|
371
448
|
registry.upgrade?.(root);
|
|
372
449
|
}
|
|
@@ -544,7 +621,7 @@ function expandSpread(node, results, options = {}) {
|
|
|
544
621
|
if (tagName === null) {
|
|
545
622
|
raiseError(`Spread binding "${result.statePathName}" requires a custom element with wcBindable, but <${element.tagName.toLowerCase()}> is not a custom element.`);
|
|
546
623
|
}
|
|
547
|
-
const registry = getCustomElementRegistry();
|
|
624
|
+
const registry = getCustomElementRegistry(element);
|
|
548
625
|
if (registry === null) {
|
|
549
626
|
raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
|
|
550
627
|
}
|
|
@@ -1122,16 +1199,33 @@ const fix = (options) => {
|
|
|
1122
1199
|
/**
|
|
1123
1200
|
* Locale number filter - formats number according to locale.
|
|
1124
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
|
+
*
|
|
1125
1219
|
* @param options - Array with locale string as first element (default: config.locale)
|
|
1126
1220
|
* @returns Filter function that returns localized number string
|
|
1127
1221
|
*/
|
|
1128
1222
|
const locale = (options) => {
|
|
1129
|
-
const
|
|
1223
|
+
const explicit = options?.[0];
|
|
1130
1224
|
return (value) => {
|
|
1131
1225
|
if (typeof value !== 'number') {
|
|
1132
1226
|
valueMustBeNumber('locale');
|
|
1133
1227
|
}
|
|
1134
|
-
return value.toLocaleString(
|
|
1228
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1135
1229
|
};
|
|
1136
1230
|
};
|
|
1137
1231
|
/**
|
|
@@ -1444,12 +1538,13 @@ const truncate = (options) => {
|
|
|
1444
1538
|
* @returns Filter function that returns date string
|
|
1445
1539
|
*/
|
|
1446
1540
|
const date = (options) => {
|
|
1447
|
-
|
|
1541
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1542
|
+
const explicit = options?.[0];
|
|
1448
1543
|
return (value) => {
|
|
1449
1544
|
if (!(value instanceof Date)) {
|
|
1450
1545
|
valueMustBeDate('date');
|
|
1451
1546
|
}
|
|
1452
|
-
return value.toLocaleDateString(
|
|
1547
|
+
return value.toLocaleDateString(explicit ?? config.locale);
|
|
1453
1548
|
};
|
|
1454
1549
|
};
|
|
1455
1550
|
/**
|
|
@@ -1459,12 +1554,13 @@ const date = (options) => {
|
|
|
1459
1554
|
* @returns Filter function that returns time string
|
|
1460
1555
|
*/
|
|
1461
1556
|
const time = (options) => {
|
|
1462
|
-
|
|
1557
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1558
|
+
const explicit = options?.[0];
|
|
1463
1559
|
return (value) => {
|
|
1464
1560
|
if (!(value instanceof Date)) {
|
|
1465
1561
|
valueMustBeDate('time');
|
|
1466
1562
|
}
|
|
1467
|
-
return value.toLocaleTimeString(
|
|
1563
|
+
return value.toLocaleTimeString(explicit ?? config.locale);
|
|
1468
1564
|
};
|
|
1469
1565
|
};
|
|
1470
1566
|
/**
|
|
@@ -1474,12 +1570,13 @@ const time = (options) => {
|
|
|
1474
1570
|
* @returns Filter function that returns datetime string
|
|
1475
1571
|
*/
|
|
1476
1572
|
const datetime = (options) => {
|
|
1477
|
-
|
|
1573
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1574
|
+
const explicit = options?.[0];
|
|
1478
1575
|
return (value) => {
|
|
1479
1576
|
if (!(value instanceof Date)) {
|
|
1480
1577
|
valueMustBeDate('datetime');
|
|
1481
1578
|
}
|
|
1482
|
-
return value.toLocaleString(
|
|
1579
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1483
1580
|
};
|
|
1484
1581
|
};
|
|
1485
1582
|
/**
|
|
@@ -2302,6 +2399,23 @@ function setLoopContextByNode(node, loopContext) {
|
|
|
2302
2399
|
loopContextByNode.set(node, loopContext);
|
|
2303
2400
|
}
|
|
2304
2401
|
|
|
2402
|
+
/**
|
|
2403
|
+
* devtools/sink.ts
|
|
2404
|
+
*
|
|
2405
|
+
* 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
|
|
2406
|
+
* 計装される側(stateElementByName / setByAddress / binding / token)と
|
|
2407
|
+
* bridge の間の循環 import を避ける。
|
|
2408
|
+
*
|
|
2409
|
+
* コスト規範(protocol §1-1): フック未接続時、計装点のコストは
|
|
2410
|
+
* `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
|
|
2411
|
+
* 必ずこのチェックの内側で行うこと。
|
|
2412
|
+
*/
|
|
2413
|
+
/** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
|
|
2414
|
+
let devtoolsSink = null;
|
|
2415
|
+
function setDevtoolsSink(sink) {
|
|
2416
|
+
devtoolsSink = sink;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2305
2419
|
const lastListValueByAbsoluteStateAddress = new WeakMap();
|
|
2306
2420
|
function getLastListValueByAbsoluteStateAddress(address) {
|
|
2307
2421
|
return lastListValueByAbsoluteStateAddress.get(address) ?? [];
|
|
@@ -2576,23 +2690,6 @@ function clearAbsoluteStateAddressByBinding(binding) {
|
|
|
2576
2690
|
absoluteStateAddressByBinding.delete(binding);
|
|
2577
2691
|
}
|
|
2578
2692
|
|
|
2579
|
-
/**
|
|
2580
|
-
* devtools/sink.ts
|
|
2581
|
-
*
|
|
2582
|
-
* 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
|
|
2583
|
-
* 計装される側(stateElementByName / setByAddress / binding / token)と
|
|
2584
|
-
* bridge の間の循環 import を避ける。
|
|
2585
|
-
*
|
|
2586
|
-
* コスト規範(protocol §1-1): フック未接続時、計装点のコストは
|
|
2587
|
-
* `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
|
|
2588
|
-
* 必ずこのチェックの内側で行うこと。
|
|
2589
|
-
*/
|
|
2590
|
-
/** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
|
|
2591
|
-
let devtoolsSink = null;
|
|
2592
|
-
function setDevtoolsSink(sink) {
|
|
2593
|
-
devtoolsSink = sink;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
2693
|
/**
|
|
2597
2694
|
* 絶対アドレス → 登録 binding の台帳。
|
|
2598
2695
|
*
|
|
@@ -3100,7 +3197,7 @@ function attachEventTokenHandler(binding) {
|
|
|
3100
3197
|
const element = binding.node;
|
|
3101
3198
|
// カスタム要素が未定義なら定義後に再試行(wcBindable が必要なため)。
|
|
3102
3199
|
const customTagName = getCustomElement(element);
|
|
3103
|
-
const registry = getCustomElementRegistry();
|
|
3200
|
+
const registry = getCustomElementRegistry(element);
|
|
3104
3201
|
if (customTagName !== null && registry?.get(customTagName) === undefined) {
|
|
3105
3202
|
if (registry === null) {
|
|
3106
3203
|
raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
|
|
@@ -3396,7 +3493,7 @@ function isPossibleTwoWay(node, propName) {
|
|
|
3396
3493
|
}
|
|
3397
3494
|
const customTagName = getCustomElement(element);
|
|
3398
3495
|
if (customTagName !== null) {
|
|
3399
|
-
const customClass = getCustomElementRegistry()?.get(customTagName);
|
|
3496
|
+
const customClass = getCustomElementRegistry(element)?.get(customTagName);
|
|
3400
3497
|
if (typeof customClass === "undefined") {
|
|
3401
3498
|
raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
|
|
3402
3499
|
}
|
|
@@ -3579,7 +3676,7 @@ function getEventName(binding) {
|
|
|
3579
3676
|
// 2.wcBindable protocol
|
|
3580
3677
|
const customTagName = getCustomElement(binding.node);
|
|
3581
3678
|
if (customTagName !== null) {
|
|
3582
|
-
const customClass = getCustomElementRegistry()?.get(customTagName);
|
|
3679
|
+
const customClass = getCustomElementRegistry(binding.node)?.get(customTagName);
|
|
3583
3680
|
if (typeof customClass === "undefined") {
|
|
3584
3681
|
raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
|
|
3585
3682
|
}
|
|
@@ -3751,7 +3848,7 @@ function addTwowayValueObserver(node, propName, observer) {
|
|
|
3751
3848
|
function attachTwowayEventHandler(binding) {
|
|
3752
3849
|
const customTagName = getCustomElement(binding.node);
|
|
3753
3850
|
if (customTagName !== null) {
|
|
3754
|
-
const registry = getCustomElementRegistry();
|
|
3851
|
+
const registry = getCustomElementRegistry(binding.node);
|
|
3755
3852
|
const customClass = registry?.get(customTagName);
|
|
3756
3853
|
if (typeof customClass === "undefined") {
|
|
3757
3854
|
if (registry === null) {
|
|
@@ -3777,7 +3874,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
3777
3874
|
function detachTwowayEventHandler(binding) {
|
|
3778
3875
|
const customTagName = getCustomElement(binding.node);
|
|
3779
3876
|
if (customTagName !== null) {
|
|
3780
|
-
const registry = getCustomElementRegistry();
|
|
3877
|
+
const registry = getCustomElementRegistry(binding.node);
|
|
3781
3878
|
const customClass = registry?.get(customTagName);
|
|
3782
3879
|
if (typeof customClass === "undefined") {
|
|
3783
3880
|
if (registry === null) {
|
|
@@ -4162,7 +4259,9 @@ function propagateListPathToOuterState(innerStateElement, innerPath) {
|
|
|
4162
4259
|
if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
|
|
4163
4260
|
return;
|
|
4164
4261
|
}
|
|
4165
|
-
|
|
4262
|
+
// source="internal": 翻訳済みの外側パスであり、書き手が書いた文字列ではない。
|
|
4263
|
+
// 存在検査に掛けても直せる相手が居ないので掛けない(pathDiagnostics.ts)。
|
|
4264
|
+
outerAbsPathInfo.stateElement.setPathInfo(outerAbsPathInfo.pathInfo.path, "for", "internal");
|
|
4166
4265
|
}
|
|
4167
4266
|
/**
|
|
4168
4267
|
* 子スコープのリスト行パス(`items.*.name`)に対応する親スコープの絶対パス情報を返す。
|
|
@@ -4546,6 +4645,16 @@ function addInterestedSession(node, session) {
|
|
|
4546
4645
|
}
|
|
4547
4646
|
interestedSessionsByNode.set(node, new Set([current, session]));
|
|
4548
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* このノードに既にバインドが張られているか。
|
|
4650
|
+
*
|
|
4651
|
+
* binder プロトコル(`bind()`)の冪等判定に使う。`remember` が binding ごとに
|
|
4652
|
+
* `addInterestedSession(binding.replaceNode, …)` を呼ぶので、バインド済みノードは
|
|
4653
|
+
* 必ずこの台帳に載っている。新しい台帳を足さずに済むぶん、二重管理の齟齬が無い。
|
|
4654
|
+
*/
|
|
4655
|
+
function hasInterestedSession(node) {
|
|
4656
|
+
return interestedSessionsByNode.has(node);
|
|
4657
|
+
}
|
|
4549
4658
|
function forEachInterestedSession(node, callback) {
|
|
4550
4659
|
const current = interestedSessionsByNode.get(node);
|
|
4551
4660
|
if (typeof current === "undefined")
|
|
@@ -4799,7 +4908,7 @@ class BindingSession {
|
|
|
4799
4908
|
return true;
|
|
4800
4909
|
}
|
|
4801
4910
|
deferUntilDefined(node, tagName, callback, reject = () => undefined) {
|
|
4802
|
-
const registry = getCustomElementRegistry();
|
|
4911
|
+
const registry = getCustomElementRegistry(node);
|
|
4803
4912
|
if (registry === null) {
|
|
4804
4913
|
raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
|
|
4805
4914
|
}
|
|
@@ -5253,7 +5362,7 @@ class BindingSession {
|
|
|
5253
5362
|
attach();
|
|
5254
5363
|
return;
|
|
5255
5364
|
}
|
|
5256
|
-
const registry = getCustomElementRegistry();
|
|
5365
|
+
const registry = getCustomElementRegistry(record.info.node);
|
|
5257
5366
|
if (registry === null) {
|
|
5258
5367
|
raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
|
|
5259
5368
|
}
|
|
@@ -5626,7 +5735,7 @@ function getWcBindable(element) {
|
|
|
5626
5735
|
if (customTagName === null) {
|
|
5627
5736
|
return null;
|
|
5628
5737
|
}
|
|
5629
|
-
const customClass = getCustomElementRegistry()?.get(customTagName);
|
|
5738
|
+
const customClass = getCustomElementRegistry(element)?.get(customTagName);
|
|
5630
5739
|
if (typeof customClass === "undefined") {
|
|
5631
5740
|
raiseError(`Custom element <${customTagName}> is not defined for command binding.`);
|
|
5632
5741
|
}
|
|
@@ -6430,8 +6539,23 @@ class Content {
|
|
|
6430
6539
|
let anchor = targetNode;
|
|
6431
6540
|
for (const node of this._movableNodes()) {
|
|
6432
6541
|
if (anchor.nextSibling !== node) {
|
|
6542
|
+
// moveBefore も childList mutation record を出すため、マークは両分岐の前
|
|
6433
6543
|
markObserverSkipOnAdd(node);
|
|
6434
|
-
|
|
6544
|
+
// moveBefore は取り外しを伴わない移動 — 接続済み行の reorder で
|
|
6545
|
+
// フォーカス・iframe・アニメーション状態を保存する(docs/a11y-design.md §4-1)。
|
|
6546
|
+
// この 1 文は 4 つのノード状態を共有する: (a) 接続済み reorder、
|
|
6547
|
+
// (b) clone フラグメント由来(root 違い)、(c) プール/unmount 済み(親なし)、
|
|
6548
|
+
// (d) バッチフラグメント内。moveBefore は「同一ツリー・親あり」を要求し
|
|
6549
|
+
// (b)(c)(d) では HierarchyRequestError を投げるため、same-parent ガード
|
|
6550
|
+
// (同 root かつ親が非 null の同時証明 = フォーカス保存が意味を持つ (a) と
|
|
6551
|
+
// 正確に一致)は外せない。ガードを外す「簡略化」をしてはならない。
|
|
6552
|
+
const mover = parentNode;
|
|
6553
|
+
if (node.parentNode === parentNode && typeof mover.moveBefore === "function") {
|
|
6554
|
+
mover.moveBefore(node, anchor.nextSibling);
|
|
6555
|
+
}
|
|
6556
|
+
else {
|
|
6557
|
+
parentNode.insertBefore(node, anchor.nextSibling);
|
|
6558
|
+
}
|
|
6435
6559
|
}
|
|
6436
6560
|
anchor = node;
|
|
6437
6561
|
}
|
|
@@ -6643,6 +6767,157 @@ function createContent(bindingInfo) {
|
|
|
6643
6767
|
return content;
|
|
6644
6768
|
}
|
|
6645
6769
|
|
|
6770
|
+
// ===========================================================================
|
|
6771
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
6772
|
+
// Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
|
|
6773
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
6774
|
+
// ===========================================================================
|
|
6775
|
+
// transition-runner protocol — how a package that mutates the DOM hands that
|
|
6776
|
+
// mutation to whoever is arbitrating view transitions on the page.
|
|
6777
|
+
//
|
|
6778
|
+
// @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
|
|
6779
|
+
// (zero runtime dependencies, independently publishable), so the arbiter installs
|
|
6780
|
+
// itself on a well-known global symbol and the participants look it up lazily.
|
|
6781
|
+
// No arbiter installed means the mutation is invoked directly, synchronously —
|
|
6782
|
+
// byte-for-byte the behavior these packages had before the protocol existed.
|
|
6783
|
+
//
|
|
6784
|
+
// docs/view-transition-design.md §4 is the normative description.
|
|
6785
|
+
//
|
|
6786
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
|
|
6787
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
6788
|
+
// (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
|
|
6789
|
+
/**
|
|
6790
|
+
* Global key the arbiter installs itself under. `Symbol.for` so independently
|
|
6791
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
6792
|
+
*/
|
|
6793
|
+
const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
|
|
6794
|
+
/**
|
|
6795
|
+
* The installed arbiter, or null when there is none, it speaks a version this
|
|
6796
|
+
* reader does not, or it does not accept this participant.
|
|
6797
|
+
*
|
|
6798
|
+
* Looked up on every call rather than cached: the tag can be added, removed, or
|
|
6799
|
+
* reconfigured at any point in a page's life, and a stale cache would either
|
|
6800
|
+
* animate what the author just switched off or miss what they switched on.
|
|
6801
|
+
*/
|
|
6802
|
+
function getTransitionRunner(source) {
|
|
6803
|
+
const candidate = globalThis[TRANSITION_RUNNER_KEY];
|
|
6804
|
+
if (candidate === undefined || candidate === null)
|
|
6805
|
+
return null;
|
|
6806
|
+
if (candidate.protocol !== "wcs-transition-runner")
|
|
6807
|
+
return null;
|
|
6808
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
6809
|
+
return null;
|
|
6810
|
+
if (typeof candidate.run !== "function")
|
|
6811
|
+
return null;
|
|
6812
|
+
if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
|
|
6813
|
+
return null;
|
|
6814
|
+
return candidate;
|
|
6815
|
+
}
|
|
6816
|
+
/**
|
|
6817
|
+
* Run `mutate` under the installed arbiter, or directly when there is none.
|
|
6818
|
+
*
|
|
6819
|
+
* Returns `undefined` in the no-arbiter case instead of a resolved promise: the
|
|
6820
|
+
* state drain calls this on every batch, and awaiting is a caller's choice, not
|
|
6821
|
+
* an allocation the common path should pay for. `await` accepts both.
|
|
6822
|
+
*/
|
|
6823
|
+
function runTransition(source, mutate, types) {
|
|
6824
|
+
const runner = getTransitionRunner(source);
|
|
6825
|
+
if (runner === null) {
|
|
6826
|
+
mutate();
|
|
6827
|
+
return undefined;
|
|
6828
|
+
}
|
|
6829
|
+
return runner.run(mutate, { source, types });
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
/** Elements that already carry a generated name (never renamed). */
|
|
6833
|
+
const namedElements = new WeakSet();
|
|
6834
|
+
/**
|
|
6835
|
+
* The generated-name ledger is per *document*, not per module instance.
|
|
6836
|
+
*
|
|
6837
|
+
* `view-transition-name` has to be unique across the whole document: the moment
|
|
6838
|
+
* two elements share one, the browser aborts the transition outright. A
|
|
6839
|
+
* module-scope counter breaks that as soon as `@wcstack/state` is loaded twice on
|
|
6840
|
+
* one page (two CDN bundles), because both copies would start minting
|
|
6841
|
+
* `wcs-row-1`. The transition-runner key is a `Symbol.for` for exactly this
|
|
6842
|
+
* reason, and the counter needs the same protection.
|
|
6843
|
+
*
|
|
6844
|
+
* Sharing the cap is right for the same reason: the cost a cap exists to bound —
|
|
6845
|
+
* one snapshot group per named element — is a document-wide cost, not a
|
|
6846
|
+
* per-bundle one.
|
|
6847
|
+
*/
|
|
6848
|
+
const NAMING_LEDGER_KEY = Symbol.for("wcstack.state.view-transition-naming");
|
|
6849
|
+
function getLedger() {
|
|
6850
|
+
const slot = globalThis;
|
|
6851
|
+
return (slot[NAMING_LEDGER_KEY] ??= { counter: 0, assigned: 0, warned: false });
|
|
6852
|
+
}
|
|
6853
|
+
/**
|
|
6854
|
+
* The active auto-naming policy, or null when names are the author's business
|
|
6855
|
+
* (the default) — one arbiter lookup per structural apply, not per row.
|
|
6856
|
+
*/
|
|
6857
|
+
function getAutoNaming() {
|
|
6858
|
+
const runner = getTransitionRunner("state");
|
|
6859
|
+
if (runner === null || runner.naming !== "auto") {
|
|
6860
|
+
return null;
|
|
6861
|
+
}
|
|
6862
|
+
return { limit: runner.namingLimit };
|
|
6863
|
+
}
|
|
6864
|
+
function firstElementOf(content) {
|
|
6865
|
+
const first = content.firstNode;
|
|
6866
|
+
if (first === null) {
|
|
6867
|
+
return null;
|
|
6868
|
+
}
|
|
6869
|
+
const last = content.lastNode;
|
|
6870
|
+
for (let node = first; node !== null; node = node.nextSibling) {
|
|
6871
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6872
|
+
return node;
|
|
6873
|
+
}
|
|
6874
|
+
if (node === last) {
|
|
6875
|
+
break;
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return null;
|
|
6879
|
+
}
|
|
6880
|
+
/**
|
|
6881
|
+
* Give this content's first element a unique name plus a class for group
|
|
6882
|
+
* styling, unless it already has one or the cap has been reached.
|
|
6883
|
+
*
|
|
6884
|
+
* The cap exists because every named element becomes its own snapshot group; a
|
|
6885
|
+
* few hundred of them make a transition visibly slow. Past it naming stops and
|
|
6886
|
+
* says so once — silently degrading would leave the author wondering why only
|
|
6887
|
+
* the first part of a list animates.
|
|
6888
|
+
*/
|
|
6889
|
+
function applyTransitionName(content, kind, naming) {
|
|
6890
|
+
const element = firstElementOf(content);
|
|
6891
|
+
if (element === null || namedElements.has(element)) {
|
|
6892
|
+
return;
|
|
6893
|
+
}
|
|
6894
|
+
// A node without `style` (anything outside HTMLElement / SVGElement) cannot
|
|
6895
|
+
// carry a name. Bail before touching the ledger: consuming the cap and marking
|
|
6896
|
+
// the element as named would burn a slot for a name that was never written,
|
|
6897
|
+
// and leave that element permanently ineligible.
|
|
6898
|
+
const style = element.style;
|
|
6899
|
+
if (style === undefined) {
|
|
6900
|
+
return;
|
|
6901
|
+
}
|
|
6902
|
+
const ledger = getLedger();
|
|
6903
|
+
if (ledger.assigned >= naming.limit) {
|
|
6904
|
+
if (!ledger.warned) {
|
|
6905
|
+
ledger.warned = true;
|
|
6906
|
+
console.warn(`[@wcstack/state] auto view-transition-name limit (${naming.limit}) reached; ` +
|
|
6907
|
+
"further elements are left unnamed. Raise naming-limit on <wcs-view-transition>, " +
|
|
6908
|
+
'or switch to naming="manual" and name only what should morph.');
|
|
6909
|
+
}
|
|
6910
|
+
return;
|
|
6911
|
+
}
|
|
6912
|
+
namedElements.add(element);
|
|
6913
|
+
ledger.assigned += 1;
|
|
6914
|
+
ledger.counter += 1;
|
|
6915
|
+
style.setProperty("view-transition-name", `wcs-${kind}-${ledger.counter}`);
|
|
6916
|
+
// Group handle for CSS (`::view-transition-group(*.wcs-row)`). Ignored by
|
|
6917
|
+
// engines that predate view-transition-class, which costs nothing.
|
|
6918
|
+
style.setProperty("view-transition-class", `wcs-${kind}`);
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6646
6921
|
const lastNodeByNode = new WeakMap();
|
|
6647
6922
|
const contentByListIndexByNode = new WeakMap();
|
|
6648
6923
|
const pooledContentsByNode = new WeakMap();
|
|
@@ -6810,6 +7085,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6810
7085
|
setRootNodeByFragment(fragment, context.rootNode);
|
|
6811
7086
|
}
|
|
6812
7087
|
const ssrMode = inSsr();
|
|
7088
|
+
// 自動命名ポリシーは行ごとではなく apply ごとに 1 回だけ引く
|
|
7089
|
+
// (docs/view-transition-design.md §6)。既定の manual では null で、
|
|
7090
|
+
// 以降の行ループは分岐 1 つ分しか増えない。
|
|
7091
|
+
const autoNaming = getAutoNaming();
|
|
6813
7092
|
const uuid = bindingInfo.uuid ?? '';
|
|
6814
7093
|
// 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
|
|
6815
7094
|
// 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
|
|
@@ -6853,6 +7132,9 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6853
7132
|
}
|
|
6854
7133
|
// コンテントを活性化
|
|
6855
7134
|
activateContent(content, loopContext, context);
|
|
7135
|
+
if (autoNaming !== null) {
|
|
7136
|
+
applyTransitionName(content, "row", autoNaming);
|
|
7137
|
+
}
|
|
6856
7138
|
});
|
|
6857
7139
|
if (typeof content === 'undefined') {
|
|
6858
7140
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
@@ -6952,6 +7234,11 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
6952
7234
|
}
|
|
6953
7235
|
const loopContext = getLoopContextByNode(bindingInfo.node);
|
|
6954
7236
|
activateContent(content, loopContext, context);
|
|
7237
|
+
// 自動命名(docs/view-transition-design.md §6)。manual(既定)では null。
|
|
7238
|
+
const autoNaming = getAutoNaming();
|
|
7239
|
+
if (autoNaming !== null) {
|
|
7240
|
+
applyTransitionName(content, "branch", autoNaming);
|
|
7241
|
+
}
|
|
6955
7242
|
}
|
|
6956
7243
|
}
|
|
6957
7244
|
|
|
@@ -6969,7 +7256,7 @@ function getInputAttributeMirror(element, propName) {
|
|
|
6969
7256
|
if (customTagName === null) {
|
|
6970
7257
|
return null;
|
|
6971
7258
|
}
|
|
6972
|
-
const customClass = getCustomElementRegistry()?.get(customTagName);
|
|
7259
|
+
const customClass = getCustomElementRegistry(element)?.get(customTagName);
|
|
6973
7260
|
if (typeof customClass === "undefined") {
|
|
6974
7261
|
return null;
|
|
6975
7262
|
}
|
|
@@ -7329,6 +7616,281 @@ function applyChangeToWebComponent(binding, _context, _newValue) {
|
|
|
7329
7616
|
});
|
|
7330
7617
|
}
|
|
7331
7618
|
|
|
7619
|
+
/**
|
|
7620
|
+
* pathDiagnostics.ts — バインド / `$watch` 対象パスの存在検査(silent failure の可視化)。
|
|
7621
|
+
*
|
|
7622
|
+
* なぜ必要か:
|
|
7623
|
+
* `getByAddress` は「親が null / undefined のパスの読み」を undefined で返し、
|
|
7624
|
+
* undefined はプロパティ書き込みがスキップされる値なので、`user.nmae` のような
|
|
7625
|
+
* 打ち間違いは**エラーも警告も出さずに DOM が更新されない**だけになる。一方で
|
|
7626
|
+
* トップレベルの打ち間違い(`cout`)は parentAddress を辿れず raiseError で落ちる。
|
|
7627
|
+
* 同じ「パスを打ち間違えた」という 1 つの失敗が、パスの深さで silent / loud に
|
|
7628
|
+
* 割れており、書き手からは区別がつかない。ここはその silent 側を埋める。
|
|
7629
|
+
*
|
|
7630
|
+
* 精度方針(過小近似):
|
|
7631
|
+
* 「確実に存在しない」と言い切れる場合にだけ報告する。getter の戻り値の先・
|
|
7632
|
+
* 空配列・null 親・mapped な `bind-component` など、静的に決められない形はすべて
|
|
7633
|
+
* `"unknown"` に倒して黙る(偽陽性ゼロ優先。docs/static-wiring-dx-design.md D7 /
|
|
7634
|
+
* [ADR-06](../../docs/architecture-hardening/06-path-type-safety.md) の精度哲学)。
|
|
7635
|
+
*
|
|
7636
|
+
* 診断 code はコンソール → lint → IDE の三面で共有する(errorGuidance.ts の規約)。
|
|
7637
|
+
*/
|
|
7638
|
+
const UNKNOWN = Object.freeze({
|
|
7639
|
+
existence: "unknown",
|
|
7640
|
+
missingSegment: "",
|
|
7641
|
+
candidates: Object.freeze([]),
|
|
7642
|
+
});
|
|
7643
|
+
const EXISTS = Object.freeze({
|
|
7644
|
+
existence: "exists",
|
|
7645
|
+
missingSegment: "",
|
|
7646
|
+
candidates: Object.freeze([]),
|
|
7647
|
+
});
|
|
7648
|
+
/**
|
|
7649
|
+
* `obj` 自身+プロトタイプチェーン(Object.prototype 手前まで)から descriptor を引く。
|
|
7650
|
+
* 打ち切り位置は getAllPropertyDescriptors と同じ — 「state が宣言したもの」だけを
|
|
7651
|
+
* 存在とみなし、`toString` 等の Object.prototype 由来を存在扱いしない。
|
|
7652
|
+
*/
|
|
7653
|
+
function findDescriptor(obj, key) {
|
|
7654
|
+
let proto = obj;
|
|
7655
|
+
while (proto !== null && proto !== Object.prototype) {
|
|
7656
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
7657
|
+
if (typeof descriptor !== "undefined") {
|
|
7658
|
+
return descriptor;
|
|
7659
|
+
}
|
|
7660
|
+
proto = Object.getPrototypeOf(proto);
|
|
7661
|
+
}
|
|
7662
|
+
return undefined;
|
|
7663
|
+
}
|
|
7664
|
+
/** `obj` 自身+プロトタイプチェーンのキー名(did-you-mean の候補集合) */
|
|
7665
|
+
function ownKeys(obj) {
|
|
7666
|
+
const keys = [];
|
|
7667
|
+
let proto = obj;
|
|
7668
|
+
while (proto !== null && proto !== Object.prototype) {
|
|
7669
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
7670
|
+
keys.push(key);
|
|
7671
|
+
}
|
|
7672
|
+
proto = Object.getPrototypeOf(proto);
|
|
7673
|
+
}
|
|
7674
|
+
return keys;
|
|
7675
|
+
}
|
|
7676
|
+
/**
|
|
7677
|
+
* 失敗した階層の兄弟候補。生オブジェクトのキーに加え、その階層にフラット宣言
|
|
7678
|
+
* (ドットパス getter)されているものも混ぜる — `cart.items.*.subtotl` の正解
|
|
7679
|
+
* `subtotal` は行オブジェクトには無く getterPaths にしか居ないため。
|
|
7680
|
+
*/
|
|
7681
|
+
function collectCandidates(container, parentPrefix, declaredPaths) {
|
|
7682
|
+
const candidates = ownKeys(container);
|
|
7683
|
+
const prefix = parentPrefix.length > 0 ? parentPrefix + DELIMITER : "";
|
|
7684
|
+
for (const declared of declaredPaths) {
|
|
7685
|
+
if (prefix.length > 0 && !declared.startsWith(prefix)) {
|
|
7686
|
+
continue;
|
|
7687
|
+
}
|
|
7688
|
+
const rest = declared.slice(prefix.length);
|
|
7689
|
+
// 直下の 1 セグメントだけを候補にする(孫は別階層の名前なので提案しない)
|
|
7690
|
+
if (rest.length > 0 && rest.indexOf(DELIMITER) === -1) {
|
|
7691
|
+
candidates.push(rest);
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
return candidates;
|
|
7695
|
+
}
|
|
7696
|
+
/**
|
|
7697
|
+
* `target` に対して `path` が解決しうるかを、値を読まずに(getter を評価せずに)判定する。
|
|
7698
|
+
*
|
|
7699
|
+
* 解決の順序は `getByAddress` の実装に合わせる: まず「パス文字列そのものがキーか」
|
|
7700
|
+
* (ドットパス getter がこれ)、次にセグメントを 1 つずつ降りる。
|
|
7701
|
+
*/
|
|
7702
|
+
function resolvePathExistence(target, path, declaredPaths) {
|
|
7703
|
+
// ドットパス getter / フラットキーの完全一致(`get "users.*.fullName"()` 等)
|
|
7704
|
+
if (findDescriptor(target, path) !== undefined) {
|
|
7705
|
+
return EXISTS;
|
|
7706
|
+
}
|
|
7707
|
+
const segments = getPathInfo(path).segments;
|
|
7708
|
+
let current = target;
|
|
7709
|
+
let prefix = "";
|
|
7710
|
+
for (let i = 0; i < segments.length; i++) {
|
|
7711
|
+
const segment = segments[i];
|
|
7712
|
+
const parentPrefix = prefix;
|
|
7713
|
+
prefix = i === 0 ? segment : prefix + DELIMITER + segment;
|
|
7714
|
+
// 途中のプレフィックスがフラット宣言されている(`cart.totalPrice` が getter で、
|
|
7715
|
+
// その戻り値のサブプロパティを読む形)。戻り値の形は評価しないと分からない
|
|
7716
|
+
if (i > 0 && i < segments.length - 1 && findDescriptor(target, prefix) !== undefined) {
|
|
7717
|
+
return UNKNOWN;
|
|
7718
|
+
}
|
|
7719
|
+
// null / undefined / primitive より深い読みは実行時 undefined 解決 = 判定不能。
|
|
7720
|
+
// 「初期値 null のオブジェクトに後から代入する」形を偽陽性で潰さないため
|
|
7721
|
+
if (Object(current) !== current) {
|
|
7722
|
+
return UNKNOWN;
|
|
7723
|
+
}
|
|
7724
|
+
if (segment === WILDCARD) {
|
|
7725
|
+
// 行の形は「いま入っている要素」からしか分からない。空配列・非配列は判定不能
|
|
7726
|
+
if (!Array.isArray(current) || current.length === 0) {
|
|
7727
|
+
return UNKNOWN;
|
|
7728
|
+
}
|
|
7729
|
+
current = current[0];
|
|
7730
|
+
continue;
|
|
7731
|
+
}
|
|
7732
|
+
const descriptor = findDescriptor(current, segment);
|
|
7733
|
+
if (typeof descriptor === "undefined") {
|
|
7734
|
+
return {
|
|
7735
|
+
existence: "missing",
|
|
7736
|
+
missingSegment: segment,
|
|
7737
|
+
candidates: collectCandidates(current, parentPrefix, declaredPaths),
|
|
7738
|
+
};
|
|
7739
|
+
}
|
|
7740
|
+
if (typeof descriptor.get === "function") {
|
|
7741
|
+
// getter の戻り値の先は評価しないと分からない(末尾なら存在は確定)
|
|
7742
|
+
return i === segments.length - 1 ? EXISTS : UNKNOWN;
|
|
7743
|
+
}
|
|
7744
|
+
current = descriptor.value;
|
|
7745
|
+
}
|
|
7746
|
+
return EXISTS;
|
|
7747
|
+
}
|
|
7748
|
+
/** 診断 code は lint / IDE と同一語彙(errorGuidance.ts の三面共有規約) */
|
|
7749
|
+
const DIAGNOSTIC_CODE = {
|
|
7750
|
+
binding: "wcs/binding-path-missing",
|
|
7751
|
+
watch: "wcs/watch-path-missing",
|
|
7752
|
+
};
|
|
7753
|
+
const SUBJECT = {
|
|
7754
|
+
binding: "Bound path",
|
|
7755
|
+
watch: "$watch path",
|
|
7756
|
+
};
|
|
7757
|
+
/**
|
|
7758
|
+
* ルート直下(単一セグメント)のパスが state に無いときのエラーメッセージ。
|
|
7759
|
+
*
|
|
7760
|
+
* この形だけは親アドレスを辿れないので読み取りが throw する = 元から loud だが、
|
|
7761
|
+
* 文面が `address.parentAddress is undefined path: cout` という内部実装の言葉で、
|
|
7762
|
+
* 「打ち間違い」だと分からず did-you-mean も lint 誘導も無かった。深いパスの
|
|
7763
|
+
* `console.warn` と同じ語彙に揃える。
|
|
7764
|
+
*/
|
|
7765
|
+
function missingRootPathMessage(stateName, path, target, declaredPaths) {
|
|
7766
|
+
return `[${DIAGNOSTIC_CODE.binding}] Path "${path}" does not exist on state "${stateName}".` +
|
|
7767
|
+
`${didYouMean(path, collectCandidates(target, "", declaredPaths))}${LINT_HINT}`;
|
|
7768
|
+
}
|
|
7769
|
+
/**
|
|
7770
|
+
* `$resolve` / `$getAll` に渡した添字の本数がワイルドカードの本数と噛み合わない。
|
|
7771
|
+
*
|
|
7772
|
+
* 不足(`$resolve`)は元から throw していたが、**超過は両 API とも黙って無視**され、
|
|
7773
|
+
* 取り違えた添字のまま「もっともらしい値」を返していた。本数はパス文字列から
|
|
7774
|
+
* 決まるので、噛み合わないことは常にプログラマのミス。
|
|
7775
|
+
*/
|
|
7776
|
+
function indexArityMessage(api, path, wildcardCount, actual) {
|
|
7777
|
+
// `$getAll` / `$setAll` の添字は前方一致の接頭辞なので上限、`$resolve` だけが厳密一致
|
|
7778
|
+
// (docs/state-set-all-design.md §4)。
|
|
7779
|
+
const requirement = api === "$resolve"
|
|
7780
|
+
? `exactly ${wildcardCount}`
|
|
7781
|
+
: `at most ${wildcardCount}`;
|
|
7782
|
+
return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
|
|
7783
|
+
`("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
|
|
7784
|
+
}
|
|
7785
|
+
/**
|
|
7786
|
+
* `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
|
|
7787
|
+
* 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
|
|
7788
|
+
* 文脈が添字を持っている場合、黙って全展開に倒すと「文脈で絞られている」という
|
|
7789
|
+
* 書き手の期待と食い違い、異なる文脈の添字の流用とも区別が付かないため throw する。
|
|
7790
|
+
*
|
|
7791
|
+
* 実行時の評価文脈に依存する(`$setAll` の spread 長と同種)ので lint へは誘導しない。
|
|
7792
|
+
*/
|
|
7793
|
+
function getAllContextMismatchMessage(path, contextPath) {
|
|
7794
|
+
return `$getAll("${path}") was called without indexes inside the loop context of ` +
|
|
7795
|
+
`"${contextPath}", but the path shares no wildcard level with that context, ` +
|
|
7796
|
+
`so the context indexes ($1..$n) do not apply. ` +
|
|
7797
|
+
`Pass indexes explicitly ([] expands every level).`;
|
|
7798
|
+
}
|
|
7799
|
+
/**
|
|
7800
|
+
* `$setAll(path, indexes, values, { spread: true })` の配列長がマッチ件数と噛み合わない。
|
|
7801
|
+
*
|
|
7802
|
+
* 静的には件数が分からない(実行時のリスト長に依存する)ので lint へは誘導しない。
|
|
7803
|
+
* 黙って切り詰める/余りを捨てると誤配が通ってしまうため throw する
|
|
7804
|
+
* (docs/state-set-all-design.md §3-3)。
|
|
7805
|
+
*/
|
|
7806
|
+
function setAllSpreadArityMessage(path, matched, actual) {
|
|
7807
|
+
return `$setAll("${path}", …, { spread: true }) requires the values array to have ` +
|
|
7808
|
+
`exactly one entry per matched address (matched ${matched}) but got ${actual}. ` +
|
|
7809
|
+
`Did the list change between $getAll and $setAll?`;
|
|
7810
|
+
}
|
|
7811
|
+
/**
|
|
7812
|
+
* `$setAll` の値と `options` の組み合わせが意味を成さない。
|
|
7813
|
+
* (docs/state-set-all-design.md §3-1)
|
|
7814
|
+
*/
|
|
7815
|
+
function setAllValueKindMessage(path, reason) {
|
|
7816
|
+
return `$setAll("${path}") ${reason}`;
|
|
7817
|
+
}
|
|
7818
|
+
/**
|
|
7819
|
+
* ワイルドカードを解決するループ文脈が足りない(=パスの階数 > スコープの階数)。
|
|
7820
|
+
*
|
|
7821
|
+
* `matrix.*.*` を 1 段の `for` の中で読む、`$2` を 1 段のループの中で読む、といった
|
|
7822
|
+
* 取り違えがこれ。元の文面は `address.listIndex?.index is undefined path: matrix.*` /
|
|
7823
|
+
* `Index not found at position 1 for loopContext:` という内部実装の言葉で、
|
|
7824
|
+
* **何を間違えたのかが書かれていなかった**。
|
|
7825
|
+
*/
|
|
7826
|
+
function wildcardScopeMessage(subject, needed, available) {
|
|
7827
|
+
return `[wcs/wildcard-rank] ${subject} needs ${needed} enclosing loop level(s) but the current ` +
|
|
7828
|
+
`scope provides ${available}. Wrap it in that many "for" templates, or use $resolve(path, indexes) ` +
|
|
7829
|
+
`to name the row explicitly.${LINT_HINT}`;
|
|
7830
|
+
}
|
|
7831
|
+
/** 同じ (state 要素, パス) の報告は 1 回だけにする台帳 */
|
|
7832
|
+
const reportedPathsByStateElement = new WeakMap();
|
|
7833
|
+
function alreadyReported(stateElement, path) {
|
|
7834
|
+
let reported = reportedPathsByStateElement.get(stateElement);
|
|
7835
|
+
if (typeof reported === "undefined") {
|
|
7836
|
+
reported = new Set();
|
|
7837
|
+
reportedPathsByStateElement.set(stateElement, reported);
|
|
7838
|
+
}
|
|
7839
|
+
if (reported.has(path)) {
|
|
7840
|
+
return true;
|
|
7841
|
+
}
|
|
7842
|
+
reported.add(path);
|
|
7843
|
+
return false;
|
|
7844
|
+
}
|
|
7845
|
+
/**
|
|
7846
|
+
* バインド確立時 / `$watch` 宣言時にパスの存在を検査し、確実に存在しないものだけ報告する。
|
|
7847
|
+
*
|
|
7848
|
+
* 報告は `console.warn` に留める(`raiseError` にしない):
|
|
7849
|
+
* 判定は過小近似とはいえ動的にキーが生える形まで排除できたわけではなく、
|
|
7850
|
+
* 既存ページを起動不能にする代償に見合わない。silent を破ることが目的であり、
|
|
7851
|
+
* 停止させることではない。
|
|
7852
|
+
*/
|
|
7853
|
+
function checkDeclaredPath(stateElement, state, path, source) {
|
|
7854
|
+
if (source === "internal" || typeof state === "undefined") {
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
// `$command` / `$streamStatus` / `$1` 等の予約名前空間は raw state に実体を持たない
|
|
7858
|
+
if (path.startsWith("$")) {
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7861
|
+
// mapped な bind-component の子スコープはパスの正本を持たない(親側で解決される)
|
|
7862
|
+
if (stateElement.hasMappedComponentState === true) {
|
|
7863
|
+
return;
|
|
7864
|
+
}
|
|
7865
|
+
// 単一セグメントのバインディングは読み取り時に raiseError で loud に落ちるので、
|
|
7866
|
+
// ここで二重に報告しない。`$watch` は落ちずに黙って発火しないだけなので検査する
|
|
7867
|
+
const segments = getPathInfo(path).segments;
|
|
7868
|
+
if (source === "binding" && segments.length < 2) {
|
|
7869
|
+
return;
|
|
7870
|
+
}
|
|
7871
|
+
if (alreadyReported(stateElement, path)) {
|
|
7872
|
+
return;
|
|
7873
|
+
}
|
|
7874
|
+
const result = resolvePathExistence(state, path, stateElement.getterPaths);
|
|
7875
|
+
if (result.existence !== "missing") {
|
|
7876
|
+
return;
|
|
7877
|
+
}
|
|
7878
|
+
// 接頭辞は raiseError と同じ `[@wcstack/state] [wcs/...]` の並び(コンソールの
|
|
7879
|
+
// grep 単位をパッケージで揃える)
|
|
7880
|
+
console.warn(`[@wcstack/state] [${DIAGNOSTIC_CODE[source]}] ${SUBJECT[source]} "${path}" does not resolve on state "${stateElement.name}": ` +
|
|
7881
|
+
`"${result.missingSegment}" is not declared.${didYouMean(result.missingSegment, result.candidates)}` +
|
|
7882
|
+
` Updates to this path will be silently dropped.${LINT_HINT}`);
|
|
7883
|
+
if (devtoolsSink !== null) {
|
|
7884
|
+
devtoolsSink({
|
|
7885
|
+
type: "state:path-unresolved",
|
|
7886
|
+
source,
|
|
7887
|
+
stateName: stateElement.name,
|
|
7888
|
+
path,
|
|
7889
|
+
missingSegment: result.missingSegment,
|
|
7890
|
+
});
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7332
7894
|
// indexName ... $1, $2, ...
|
|
7333
7895
|
function getIndexValueByLoopContext(loopContext, indexName) {
|
|
7334
7896
|
if (loopContext.listIndex === null) {
|
|
@@ -7340,7 +7902,10 @@ function getIndexValueByLoopContext(loopContext, indexName) {
|
|
|
7340
7902
|
}
|
|
7341
7903
|
const listIndex = listIndexAtWildcard(loopContext.listIndex, indexPos, loopContext.pathInfo.wildcardCount);
|
|
7342
7904
|
if (listIndex === null) {
|
|
7343
|
-
|
|
7905
|
+
// 位置が範囲外 = `$2` を 1 段のループの中で読んだ、という取り違え。
|
|
7906
|
+
// 元の文面(`Index not found at position 1 for loopContext:`)は内部の言葉で、
|
|
7907
|
+
// 何段必要で何段あるのかが書かれていなかった。
|
|
7908
|
+
raiseError(wildcardScopeMessage(`"${indexName}"`, indexPos + 1, loopContext.pathInfo.wildcardCount));
|
|
7344
7909
|
}
|
|
7345
7910
|
return listIndex.index;
|
|
7346
7911
|
}
|
|
@@ -7392,7 +7957,7 @@ function scheduleDeferredApply(binding, tagName) {
|
|
|
7392
7957
|
return;
|
|
7393
7958
|
}
|
|
7394
7959
|
// Compatibility fallback for direct applyChange() callers outside a session.
|
|
7395
|
-
const registry = getCustomElementRegistry();
|
|
7960
|
+
const registry = getCustomElementRegistry(binding.replaceNode);
|
|
7396
7961
|
if (registry === null) {
|
|
7397
7962
|
scheduledBindings.delete(binding);
|
|
7398
7963
|
reportFailure(tagName, new Error("CustomElementRegistry is unavailable."));
|
|
@@ -7536,7 +8101,7 @@ function applyChange(binding, context) {
|
|
|
7536
8101
|
if (definedApplyVerifiedByBinding.get(binding) !== true) {
|
|
7537
8102
|
const customTag = getCustomElement(binding.replaceNode);
|
|
7538
8103
|
if (customTag) {
|
|
7539
|
-
if (getCustomElementRegistry()?.get(customTag) === undefined) {
|
|
8104
|
+
if (getCustomElementRegistry(binding.replaceNode)?.get(customTag) === undefined) {
|
|
7540
8105
|
// 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
|
|
7541
8106
|
// 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
|
|
7542
8107
|
// whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
|
|
@@ -7587,6 +8152,26 @@ function applyChange(binding, context) {
|
|
|
7587
8152
|
}
|
|
7588
8153
|
}
|
|
7589
8154
|
|
|
8155
|
+
/**
|
|
8156
|
+
* バインディング 1 本の適用失敗を報告する(握り潰しではない)。
|
|
8157
|
+
*
|
|
8158
|
+
* `console.error` だけだと devtools からは「静かに握られた失敗」が見えないため、
|
|
8159
|
+
* 同じ地点から sink にも流す(`state:watch-error` と同じ位置づけ)。
|
|
8160
|
+
* 値と DOM は巻き戻さない — 伝播 hop 上限超過・watch 連鎖打ち切りと同じ姿勢。
|
|
8161
|
+
*/
|
|
8162
|
+
function reportBindingApplyError(binding, error) {
|
|
8163
|
+
console.error(`[@wcstack/state] binding "${binding.bindingType}: ${binding.statePathName}" failed to apply; ` +
|
|
8164
|
+
`the rest of this batch continues.`, { node: binding.node, error });
|
|
8165
|
+
if (devtoolsSink !== null) {
|
|
8166
|
+
devtoolsSink({
|
|
8167
|
+
type: "state:binding-apply-error",
|
|
8168
|
+
stateName: binding.stateName,
|
|
8169
|
+
path: binding.statePathName,
|
|
8170
|
+
bindingType: binding.bindingType,
|
|
8171
|
+
error,
|
|
8172
|
+
});
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
7590
8175
|
/**
|
|
7591
8176
|
* バインディング情報の配列を処理し、各バインディングに対して状態の変更を適用する。
|
|
7592
8177
|
*
|
|
@@ -7642,7 +8227,16 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
7642
8227
|
propagationContextByBinding: propagationContextByBinding,
|
|
7643
8228
|
};
|
|
7644
8229
|
do {
|
|
7645
|
-
|
|
8230
|
+
// 1 本の失敗を 1 本に閉じ込める(§ エラー隔離)。隔離しないと、stale な
|
|
8231
|
+
// アドレスを読んだ 1 本の throw がバッチの残り・$updatedCallback・drain
|
|
8232
|
+
// リスナー($watch / $streams restart)まで道連れにし、「値は新しいのに
|
|
8233
|
+
// DOM は途中まで」という再現困難な半端状態を作る。
|
|
8234
|
+
try {
|
|
8235
|
+
applyChange(binding, context);
|
|
8236
|
+
}
|
|
8237
|
+
catch (error) {
|
|
8238
|
+
reportBindingApplyError(binding, error);
|
|
8239
|
+
}
|
|
7646
8240
|
bindingIndex++;
|
|
7647
8241
|
const nextBindingInfo = bindings[bindingIndex];
|
|
7648
8242
|
if (!nextBindingInfo)
|
|
@@ -7658,7 +8252,12 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
7658
8252
|
// applyChangeToProperty は propagationContextByBinding 以外の context を
|
|
7659
8253
|
// 参照しないため、遅延分は最小 context を渡す
|
|
7660
8254
|
for (const { binding, value } of deferredSelectBindings) {
|
|
7661
|
-
|
|
8255
|
+
try {
|
|
8256
|
+
applyChangeToProperty(binding, { propagationContextByBinding }, value);
|
|
8257
|
+
}
|
|
8258
|
+
catch (error) {
|
|
8259
|
+
reportBindingApplyError(binding, error);
|
|
8260
|
+
}
|
|
7662
8261
|
}
|
|
7663
8262
|
for (const [absAddress, newListValue] of newListValueByAbsAddress.entries()) {
|
|
7664
8263
|
setLastListValueByAbsoluteStateAddress(absAddress, newListValue);
|
|
@@ -8109,7 +8708,13 @@ function collectStructuralFragments(rootNode, walkRoot, forPath) {
|
|
|
8109
8708
|
async function waitForStateInitialize(root) {
|
|
8110
8709
|
const elements = root.querySelectorAll(config.tagNames.state);
|
|
8111
8710
|
const promises = [];
|
|
8112
|
-
|
|
8711
|
+
const registry = getCustomElementRegistry(root);
|
|
8712
|
+
if (registry === null) {
|
|
8713
|
+
// null レジストリのサブツリーでは <wcs-state> が upgrade されないので
|
|
8714
|
+
// initializePromise が生えず、待っても永久に初期化されない。
|
|
8715
|
+
raiseError(`CustomElementRegistry is unavailable for <${config.tagNames.state}>.`);
|
|
8716
|
+
}
|
|
8717
|
+
await registry.whenDefined(config.tagNames.state);
|
|
8113
8718
|
for (const element of elements) {
|
|
8114
8719
|
// Light DOM の mapped コンポーネントの state は待たない。それはこの root の
|
|
8115
8720
|
// バインディングが張られてからでないと初期化できず(自分を束ねるホスト binding を
|
|
@@ -8148,7 +8753,7 @@ async function buildBindings(root) {
|
|
|
8148
8753
|
}
|
|
8149
8754
|
}
|
|
8150
8755
|
|
|
8151
|
-
var version = "1.
|
|
8756
|
+
var version = "1.32.0";
|
|
8152
8757
|
var pkg = {
|
|
8153
8758
|
version: version};
|
|
8154
8759
|
|
|
@@ -8871,28 +9476,262 @@ async function hydrateBindings(root) {
|
|
|
8871
9476
|
return true;
|
|
8872
9477
|
}
|
|
8873
9478
|
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
//
|
|
8877
|
-
//
|
|
8878
|
-
//
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
9479
|
+
// ===========================================================================
|
|
9480
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
9481
|
+
// Generated from /protocol/binder.ts by scripts/sync-protocol-types.mjs.
|
|
9482
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
9483
|
+
// ===========================================================================
|
|
9484
|
+
// binder protocol — how a package that inserts DOM hands those nodes to whoever
|
|
9485
|
+
// owns data bindings on the page.
|
|
9486
|
+
//
|
|
9487
|
+
// The dual of transition-runner: that one hands a *mutation* to whoever animates
|
|
9488
|
+
// it, this one hands *new nodes* to whoever binds them.
|
|
9489
|
+
//
|
|
9490
|
+
// A `data-wcs` binding exists only for nodes @wcstack/state walked when it built
|
|
9491
|
+
// its bindings. Nodes that arrive later — the content of a route that was not
|
|
9492
|
+
// active at that moment, a <wcs-head> child reflected into <head> — were never
|
|
9493
|
+
// walked, so their bindings silently do nothing, however often they are inserted.
|
|
9494
|
+
// @wcstack/router must not depend on @wcstack/state (zero runtime dependencies,
|
|
9495
|
+
// independently publishable), so state installs a binder on a well-known global
|
|
9496
|
+
// symbol and inserters look it up lazily.
|
|
9497
|
+
//
|
|
9498
|
+
// No binder installed means nothing happens — byte-for-byte the behavior these
|
|
9499
|
+
// packages had before the protocol existed.
|
|
9500
|
+
//
|
|
9501
|
+
// docs/binder-protocol-design.md is the normative description.
|
|
9502
|
+
//
|
|
9503
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/binder.ts), then run
|
|
9504
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
9505
|
+
// (packages/<pkg>/src/protocol/binder.ts). Those copies are generated — do not edit them.
|
|
9506
|
+
/**
|
|
9507
|
+
* Global key the binder installs itself under. `Symbol.for` so independently
|
|
9508
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
9509
|
+
*/
|
|
9510
|
+
const BINDER_KEY = Symbol.for("wcstack.binder");
|
|
9511
|
+
/**
|
|
9512
|
+
* The installed binder, or null when there is none or it speaks a version this
|
|
9513
|
+
* reader does not.
|
|
9514
|
+
*
|
|
9515
|
+
* Looked up on every call rather than cached, for the same reason
|
|
9516
|
+
* transition-runner does: the page's composition can change at any point, and a
|
|
9517
|
+
* stale cache would keep calling into a binder that is no longer there.
|
|
9518
|
+
*/
|
|
9519
|
+
function getBinder() {
|
|
9520
|
+
const candidate = globalThis[BINDER_KEY];
|
|
9521
|
+
if (candidate === undefined || candidate === null)
|
|
9522
|
+
return null;
|
|
9523
|
+
if (candidate.protocol !== "wcs-binder")
|
|
9524
|
+
return null;
|
|
9525
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
9526
|
+
return null;
|
|
9527
|
+
if (typeof candidate.bind !== "function")
|
|
9528
|
+
return null;
|
|
9529
|
+
return candidate;
|
|
9530
|
+
}
|
|
9531
|
+
/**
|
|
9532
|
+
* Subtrees offered before a binder existed, and the set of everything a binder
|
|
9533
|
+
* has taken. Both live on global symbols so that independently loaded copies of
|
|
9534
|
+
* this file — the router's and state's — share one queue.
|
|
9535
|
+
*
|
|
9536
|
+
* The queue is needed because of load order: the router's auto bundle runs
|
|
9537
|
+
* before state's, so `<wcs-head>` reflects its children into `<head>` while
|
|
9538
|
+
* there is still nothing to bind them. Offering them to a binder that arrives
|
|
9539
|
+
* later is the difference between working and silently blank.
|
|
9540
|
+
*/
|
|
9541
|
+
const PENDING_KEY = Symbol.for("wcstack.binder.pending");
|
|
9542
|
+
const TAKEN_KEY = Symbol.for("wcstack.binder.taken");
|
|
9543
|
+
function pendingQueue() {
|
|
9544
|
+
const globals = globalThis;
|
|
9545
|
+
let queue = globals[PENDING_KEY];
|
|
9546
|
+
if (queue === undefined) {
|
|
9547
|
+
queue = [];
|
|
9548
|
+
globals[PENDING_KEY] = queue;
|
|
9549
|
+
}
|
|
9550
|
+
return queue;
|
|
9551
|
+
}
|
|
9552
|
+
function takenSet() {
|
|
9553
|
+
const globals = globalThis;
|
|
9554
|
+
let taken = globals[TAKEN_KEY];
|
|
9555
|
+
if (taken === undefined) {
|
|
9556
|
+
taken = new WeakSet();
|
|
9557
|
+
globals[TAKEN_KEY] = taken;
|
|
9558
|
+
}
|
|
9559
|
+
return taken;
|
|
9560
|
+
}
|
|
9561
|
+
/**
|
|
9562
|
+
* Bind everything offered before this binder existed. Called by the binder right
|
|
9563
|
+
* after it installs itself.
|
|
9564
|
+
*/
|
|
9565
|
+
function flushPendingBinds() {
|
|
9566
|
+
const binder = getBinder();
|
|
9567
|
+
if (binder === null)
|
|
9568
|
+
return;
|
|
9569
|
+
const queue = pendingQueue();
|
|
9570
|
+
if (queue.length === 0)
|
|
9571
|
+
return;
|
|
9572
|
+
const pending = queue.splice(0, queue.length);
|
|
9573
|
+
const taken = takenSet();
|
|
9574
|
+
for (const subtree of pending) {
|
|
9575
|
+
taken.add(subtree);
|
|
9576
|
+
binder.bind(subtree);
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
|
|
9580
|
+
/**
|
|
9581
|
+
* binder プロトコルの提供側(docs/binder-protocol-design.md)。
|
|
9582
|
+
*
|
|
9583
|
+
* `buildBindings` は起動時に `document.body` を 1 回走査するだけなので、そのとき
|
|
9584
|
+
* document に居なかったノードのバインドは存在しない。router が後から差し込む
|
|
9585
|
+
* ルート内容や `<wcs-head>` のクローンがこれに当たり、書いたバインドが黙って
|
|
9586
|
+
* 何もしない状態になっていた。`bind()` はその取りこぼしを 1 サブツリー分だけ
|
|
9587
|
+
* 埋める。
|
|
9588
|
+
*
|
|
9589
|
+
* **走査を勝手に広げない。** MutationObserver が見た全追加ノードを走査する形に
|
|
9590
|
+
* すると、バインドを 1 個も持たない挿入(大多数)にコストが乗り、さらに
|
|
9591
|
+
* `innerHTML` で入れた外部由来の DOM が `data-wcs` を発火させることになる。
|
|
9592
|
+
* ここで束ねるのは**明示的に渡されたものだけ**である。
|
|
9593
|
+
*/
|
|
9594
|
+
const BIND_ATTRIBUTE_SELECTOR = () => `[${config.bindAttributeName}]`;
|
|
9595
|
+
/**
|
|
9596
|
+
* このサブツリーは既にバインド済みか。
|
|
9597
|
+
*
|
|
9598
|
+
* ルート内容は「起動時に active だったので全部バインド済み」か「一度も走査されて
|
|
9599
|
+
* いないので全部未バインド」のどちらかで、途中の状態を取らない。したがって
|
|
9600
|
+
* **宣言を持つ最初のノード 1 個**を見れば足りる。全ノードを走査して判定するのは
|
|
9601
|
+
* 同じ結論により高いコストを払うだけになる。
|
|
9602
|
+
*/
|
|
9603
|
+
function alreadyBound(subtree) {
|
|
9604
|
+
if (hasInterestedSession(subtree)) {
|
|
9605
|
+
return true;
|
|
9606
|
+
}
|
|
9607
|
+
if (!isElement(subtree)) {
|
|
9608
|
+
return false;
|
|
9609
|
+
}
|
|
9610
|
+
if (subtree.hasAttribute(config.bindAttributeName)) {
|
|
9611
|
+
// 属性を持つのに台帳に居ない = 未バインド
|
|
9612
|
+
return false;
|
|
9613
|
+
}
|
|
9614
|
+
const first = subtree.querySelector(BIND_ATTRIBUTE_SELECTOR());
|
|
9615
|
+
return first !== null && hasInterestedSession(first);
|
|
9616
|
+
}
|
|
9617
|
+
function isElement(node) {
|
|
9618
|
+
return node.nodeType === 1;
|
|
9619
|
+
}
|
|
9620
|
+
function bindNow(subtree) {
|
|
9621
|
+
if (alreadyBound(subtree)) {
|
|
9622
|
+
return;
|
|
9623
|
+
}
|
|
9624
|
+
convertMustacheToComments(subtree);
|
|
9625
|
+
collectStructuralFragments(subtree.getRootNode(), subtree);
|
|
9626
|
+
// `getSubscriberNodes` の TreeWalker は**ルート自身を返さない**。`buildBindings` は
|
|
9627
|
+
// `document.body` を渡すので今まで問題にならなかったが、ここには宣言をルートに
|
|
9628
|
+
// 持つノードが来る(`<wcs-head>` が head へ入れる `<title data-wcs="…">`)。
|
|
9629
|
+
// そのときだけ親から走査して、ルートを走査範囲に含める。兄弟の重複登録は
|
|
9630
|
+
// `registeredNodeSet` が弾くので、余計なバインドは生まれない。
|
|
9631
|
+
// 親は Element とは限らない(ShadowRoot 直下なら DocumentFragment、head 直下なら
|
|
9632
|
+
// Element)。`parentElement` だと前者で null になり、ルートを含められない。
|
|
9633
|
+
const declaresOnRoot = subtree.hasAttribute(config.bindAttributeName);
|
|
9634
|
+
const parent = subtree.parentNode;
|
|
9635
|
+
const canWalkFromParent = parent !== null
|
|
9636
|
+
&& (parent.nodeType === 1 || parent.nodeType === 9 || parent.nodeType === 11);
|
|
9637
|
+
const walkRoot = declaresOnRoot && canWalkFromParent
|
|
9638
|
+
? parent
|
|
9639
|
+
: subtree;
|
|
9640
|
+
initializeBindings(walkRoot, null);
|
|
9641
|
+
}
|
|
9642
|
+
/**
|
|
9643
|
+
* 初期バインド構築より前に差し出されたサブツリー。
|
|
9644
|
+
*
|
|
9645
|
+
* `<wcs-head>` は `connectedCallback` の中でクローンを head へ入れるので、
|
|
9646
|
+
* state / router のどちらを先に読み込んでも「まだ構築が終わっていない」時点で
|
|
9647
|
+
* bind を求めてくる。そこで同期に束ねても `<wcs-state>` の登録が済んでおらず、
|
|
9648
|
+
* バインドは state を見つけられない。**構築の完了を唯一の合図にする。**
|
|
9649
|
+
*/
|
|
9650
|
+
const beforeFirstBuild = [];
|
|
9651
|
+
function bind(subtree) {
|
|
9652
|
+
if (!isElement(subtree) || alreadyBound(subtree)) {
|
|
9653
|
+
return;
|
|
9654
|
+
}
|
|
9655
|
+
if (!areBindingsBuilt(subtree.getRootNode())) {
|
|
9656
|
+
beforeFirstBuild.push(subtree);
|
|
9657
|
+
return;
|
|
9658
|
+
}
|
|
9659
|
+
bindNow(subtree);
|
|
9660
|
+
}
|
|
9661
|
+
/**
|
|
9662
|
+
* 初期バインド構築の完了時に呼ぶ(stateElementByName.ts)。binder が居ない時点で
|
|
9663
|
+
* 差し出された分(プロトコルの保留キュー)と、居たが早すぎた分をまとめて束ねる。
|
|
9664
|
+
*/
|
|
9665
|
+
function drainPendingBinds() {
|
|
9666
|
+
const pending = beforeFirstBuild.splice(0, beforeFirstBuild.length);
|
|
9667
|
+
for (const subtree of pending) {
|
|
9668
|
+
bindNow(subtree);
|
|
9669
|
+
}
|
|
9670
|
+
flushPendingBinds();
|
|
9671
|
+
}
|
|
9672
|
+
const binder = {
|
|
9673
|
+
protocol: "wcs-binder",
|
|
9674
|
+
version: 1,
|
|
9675
|
+
bind,
|
|
9676
|
+
};
|
|
9677
|
+
/**
|
|
9678
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
9679
|
+
*
|
|
9680
|
+
* 既に別のコピーが載っているなら譲る。1 ページに 2 つの state バンドルが載る構成
|
|
9681
|
+
* (CDN の取り違え)で、後から読まれた側が先客を追い出すと、先客がバインドした
|
|
9682
|
+
* ノードの台帳と食い違う。
|
|
9683
|
+
*/
|
|
9684
|
+
function registerBinder() {
|
|
9685
|
+
const globals = globalThis;
|
|
9686
|
+
if (globals[BINDER_KEY] === undefined) {
|
|
9687
|
+
globals[BINDER_KEY] = binder;
|
|
9688
|
+
}
|
|
9689
|
+
// ここでは引き取らない。`<wcs-state>` の登録は connectedCallback の await より
|
|
9690
|
+
// 後なので、この時点ではまだ state が居ない。保留分は初期バインド構築の完了時に
|
|
9691
|
+
// 流す(stateElementByName.ts)。そこが「state が確実に居る」最初の瞬間である。
|
|
9692
|
+
}
|
|
9693
|
+
|
|
9694
|
+
const stateElementByNameByNode = new WeakMap();
|
|
9695
|
+
const bindingsReadyByNode = new WeakMap();
|
|
9696
|
+
// devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
|
|
9697
|
+
// サイズは <wcs-state> 要素数に拘束され、unregister(disconnectedCallback)で
|
|
9698
|
+
// 必ず削除されるためリークしない。
|
|
9699
|
+
const liveStateElements = new Set();
|
|
9700
|
+
function getLiveStateElements() {
|
|
9701
|
+
return liveStateElements;
|
|
9702
|
+
}
|
|
9703
|
+
function getStateElementByName(rootNode, name) {
|
|
9704
|
+
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
9705
|
+
if (!stateElementByName) {
|
|
9706
|
+
return null;
|
|
9707
|
+
}
|
|
9708
|
+
return stateElementByName.get(name) || null;
|
|
9709
|
+
}
|
|
8890
9710
|
/**
|
|
8891
9711
|
* 指定された rootNode のバインディング初期化が完了するまで待機する Promise を返す。
|
|
8892
9712
|
*/
|
|
8893
9713
|
function getBindingsReady(rootNode) {
|
|
8894
9714
|
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
8895
9715
|
}
|
|
9716
|
+
const bindingsBuiltRoots = new WeakSet();
|
|
9717
|
+
/**
|
|
9718
|
+
* この rootNode の初期バインド構築が完了しているか。
|
|
9719
|
+
*
|
|
9720
|
+
* binder プロトコル(`bind()`)が使う。router の `<wcs-head>` はクローンを
|
|
9721
|
+
* `connectedCallback` の中で head へ入れるので、**state が最初の走査を終える前**に
|
|
9722
|
+
* bind を求めてくる。そこで同期に束ねても state 要素の初期化が済んでおらず、
|
|
9723
|
+
* 結果は空のままになる。完了までは binder 側で保留する。
|
|
9724
|
+
*
|
|
9725
|
+
* 「まだ登録も済んでいない」と「もう構築が終わった」を取り違えないよう、判定は
|
|
9726
|
+
* 完了の側で持つ。<wcs-state> の登録は connectedCallback の await より後に起きるので、
|
|
9727
|
+
* 「エントリの有無」で進行中かを測ると読み込み順によって逆の答えを返す。
|
|
9728
|
+
*/
|
|
9729
|
+
function areBindingsBuilt(rootNode) {
|
|
9730
|
+
return bindingsBuiltRoots.has(rootNode);
|
|
9731
|
+
}
|
|
9732
|
+
function markBindingsBuilt(rootNode) {
|
|
9733
|
+
bindingsBuiltRoots.add(rootNode);
|
|
9734
|
+
}
|
|
8896
9735
|
function setStateElementByName(rootNode, name, element) {
|
|
8897
9736
|
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
8898
9737
|
if (element === null) {
|
|
@@ -8945,6 +9784,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
8945
9784
|
else {
|
|
8946
9785
|
await buildBindings(rootNode);
|
|
8947
9786
|
}
|
|
9787
|
+
markBindingsBuilt(rootNode);
|
|
9788
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
9789
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
9790
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
9791
|
+
drainPendingBinds();
|
|
8948
9792
|
resolve();
|
|
8949
9793
|
}
|
|
8950
9794
|
catch (error) {
|
|
@@ -8959,6 +9803,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
8959
9803
|
queueMicrotask(async () => {
|
|
8960
9804
|
try {
|
|
8961
9805
|
await buildBindings(rootNode);
|
|
9806
|
+
markBindingsBuilt(rootNode);
|
|
9807
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
9808
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
9809
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
9810
|
+
drainPendingBinds();
|
|
8962
9811
|
resolve();
|
|
8963
9812
|
}
|
|
8964
9813
|
catch (error) {
|
|
@@ -9068,6 +9917,17 @@ function notifyUpdateBatchListeners(batch) {
|
|
|
9068
9917
|
registered.listener(batch);
|
|
9069
9918
|
}
|
|
9070
9919
|
}
|
|
9920
|
+
/**
|
|
9921
|
+
* 遷移越しの適用が失敗したときの報告。
|
|
9922
|
+
*
|
|
9923
|
+
* 遷移の中では例外を同期的に呼び出し元へ投げ返せない。今日の drain は
|
|
9924
|
+
* queueMicrotask の中で throw する = uncaught として観測されるので、それと同じ
|
|
9925
|
+
* 「loud に出す」挙動へ揃える。握り潰すと `$updatedCallback` の throw が黙って
|
|
9926
|
+
* 消える(README の 3 層表が定める伝播の契約が破れる)。
|
|
9927
|
+
*/
|
|
9928
|
+
function reportDeferredApplyFailure(error) {
|
|
9929
|
+
queueMicrotask(() => { throw error; });
|
|
9930
|
+
}
|
|
9071
9931
|
class Updater {
|
|
9072
9932
|
_queueUpdateRecords = [];
|
|
9073
9933
|
constructor() {
|
|
@@ -9166,17 +10026,49 @@ class Updater {
|
|
|
9166
10026
|
}
|
|
9167
10027
|
}
|
|
9168
10028
|
}
|
|
9169
|
-
// context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
|
|
9170
|
-
if (propagationContextByBinding.size > 0) {
|
|
9171
|
-
applyChangeFromBindings(processBindings, propagationContextByBinding);
|
|
9172
|
-
}
|
|
9173
|
-
else {
|
|
9174
|
-
applyChangeFromBindings(processBindings);
|
|
9175
|
-
}
|
|
9176
10029
|
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
9177
10030
|
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
9178
10031
|
// quarantine された address も state 値は適用済みのため通知対象に含める。
|
|
9179
|
-
|
|
10032
|
+
//
|
|
10033
|
+
// try/finally なのは、適用側が throw しても `$watch` / `$streams` restart を
|
|
10034
|
+
// 落とさないため。binding 1 本の失敗は applyChangeFromBindings が隔離するので
|
|
10035
|
+
// ここへ来るのは $updatedCallback の throw(契約どおり loud に伝播させる)等に
|
|
10036
|
+
// 限られるが、そのとき drain フックまで道連れにすると「機構間の順序は固定」
|
|
10037
|
+
// (README の 3 層表)が黙って破れる。例外は握らない = 伝播は維持する。
|
|
10038
|
+
try {
|
|
10039
|
+
const applyBindings = () => {
|
|
10040
|
+
// context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
|
|
10041
|
+
if (propagationContextByBinding.size > 0) {
|
|
10042
|
+
applyChangeFromBindings(processBindings, propagationContextByBinding);
|
|
10043
|
+
}
|
|
10044
|
+
else {
|
|
10045
|
+
applyChangeFromBindings(processBindings);
|
|
10046
|
+
}
|
|
10047
|
+
};
|
|
10048
|
+
// View transition 参加点(docs/view-transition-design.md §7.2)。arbiter が
|
|
10049
|
+
// 居なければ runTransition はその場で applyBindings を呼び、undefined を返す
|
|
10050
|
+
// = 従来と完全に同じ同期適用。SSR では遷移そのものを持たない(G5)。
|
|
10051
|
+
//
|
|
10052
|
+
// 適用する binding が 0 本のバッチは arbiter へ渡さない。書き込みはバインドの
|
|
10053
|
+
// 有無に関わらず enqueue される(setByAddress)ため、headless なパス
|
|
10054
|
+
// (`$watch` 専用・`$streams` の内部状態・リスト置換の中間アドレス)への
|
|
10055
|
+
// 書き込みだけでもここへ到達する。それでページ全体をスナップショットするのは
|
|
10056
|
+
// 無駄なだけでなく、既定の mode="latest" では「アニメーションすべき DOM 変更が
|
|
10057
|
+
// 無い遷移」が実行中の本物の遷移をスキップしてしまう(ルート遷移が毎回途中で
|
|
10058
|
+
// 切れる/active が空撃ちで振動する)。
|
|
10059
|
+
if (inSsr() || processBindings.length === 0) {
|
|
10060
|
+
applyBindings();
|
|
10061
|
+
}
|
|
10062
|
+
else {
|
|
10063
|
+
const pending = runTransition("state", applyBindings);
|
|
10064
|
+
if (pending !== undefined) {
|
|
10065
|
+
pending.catch(reportDeferredApplyFailure);
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
10068
|
+
}
|
|
10069
|
+
finally {
|
|
10070
|
+
notifyUpdateBatchListeners(new Set(contextByAbsoluteAddress.keys()));
|
|
10071
|
+
}
|
|
9180
10072
|
}
|
|
9181
10073
|
}
|
|
9182
10074
|
const updater = new Updater();
|
|
@@ -9566,6 +10458,58 @@ function registerDevtoolsSource() {
|
|
|
9566
10458
|
getOrCreateHookRegistry().register(source);
|
|
9567
10459
|
}
|
|
9568
10460
|
|
|
10461
|
+
/**
|
|
10462
|
+
* ssr-snapshot プロトコルの提供側(docs/ssr-router-design.md §5)。
|
|
10463
|
+
*
|
|
10464
|
+
* `<wcs-ssr>` スナップショットを document 全体に対する最終パスとして生成する。
|
|
10465
|
+
* connectedCallback 内の inline 生成は「その時点の DOM」しか見えず、router が
|
|
10466
|
+
* 後から挿入するルート内容の構造テンプレートを取り逃がすレースがあった
|
|
10467
|
+
* (state のロード方式と文書順に依存)。renderToString が全要素の完了と
|
|
10468
|
+
* バインディング構築の後にこれを呼ぶことで、スナップショットは常に確定後の
|
|
10469
|
+
* DOM を見る。
|
|
10470
|
+
*
|
|
10471
|
+
* 複数 `enable-ssr` state の意味論は inline 生成と同一に保つ(文書順に生成・
|
|
10472
|
+
* fragment レジストリはモジュール共有・props store は生成ごとにクリア)。
|
|
10473
|
+
* その整理は本プロトコルの範囲外の既存挙動として引き継ぐ。
|
|
10474
|
+
*/
|
|
10475
|
+
function buildSsrDocument(root) {
|
|
10476
|
+
const stateTag = config.tagNames.state;
|
|
10477
|
+
const ssrTag = config.tagNames.ssr;
|
|
10478
|
+
const stateElements = root.querySelectorAll(`${stateTag}[enable-ssr]`);
|
|
10479
|
+
for (const stateEl of stateElements) {
|
|
10480
|
+
const name = stateEl.getAttribute("name") || "default";
|
|
10481
|
+
// 既に直前へ生成済み(旧 server との組み合わせで inline 生成された等)なら
|
|
10482
|
+
// 何もしない — build() は冪等でなければならない(プロトコル契約)
|
|
10483
|
+
const prev = stateEl.previousElementSibling;
|
|
10484
|
+
if (prev !== null &&
|
|
10485
|
+
prev.tagName.toLowerCase() === ssrTag &&
|
|
10486
|
+
(prev.getAttribute("name") || "default") === name) {
|
|
10487
|
+
continue;
|
|
10488
|
+
}
|
|
10489
|
+
const ssrEl = document.createElement(ssrTag);
|
|
10490
|
+
ssrEl.setAttribute("name", name);
|
|
10491
|
+
ssrEl.setAttribute("version", VERSION);
|
|
10492
|
+
Ssr.buildContent(ssrEl, Ssr.extractStateData(stateEl));
|
|
10493
|
+
stateEl.parentNode?.insertBefore(ssrEl, stateEl);
|
|
10494
|
+
}
|
|
10495
|
+
}
|
|
10496
|
+
const builder = {
|
|
10497
|
+
protocol: "wcs-ssr-snapshot",
|
|
10498
|
+
version: 1,
|
|
10499
|
+
build: buildSsrDocument,
|
|
10500
|
+
};
|
|
10501
|
+
/**
|
|
10502
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
10503
|
+
* binder(registerBinder)と同じ規範 — 既に別のコピーが載っているなら譲る
|
|
10504
|
+
* (そのコピーのレジストリが、そのページの正本だからである)。
|
|
10505
|
+
*/
|
|
10506
|
+
function registerSsrSnapshotBuilder() {
|
|
10507
|
+
const globals = globalThis;
|
|
10508
|
+
if (globals[SSR_SNAPSHOT_BUILDER_KEY] === undefined) {
|
|
10509
|
+
globals[SSR_SNAPSHOT_BUILDER_KEY] = builder;
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
|
|
9569
10513
|
const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
|
|
9570
10514
|
/**
|
|
9571
10515
|
* インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
|
|
@@ -9646,9 +10590,26 @@ async function loadFromJsonFile(url) {
|
|
|
9646
10590
|
}
|
|
9647
10591
|
}
|
|
9648
10592
|
|
|
10593
|
+
/**
|
|
10594
|
+
* `src` の値を **document の base URL** に対して解決する。
|
|
10595
|
+
*
|
|
10596
|
+
* `import(url)` の相対解決は「import を書いたモジュール」を基準にする。ここは
|
|
10597
|
+
* `@wcstack/state` の中なので、素の `import(url)` は `<wcs-state src>` を
|
|
10598
|
+
* **state パッケージの所在**から解決してしまう。同一オリジンに置いたページでは
|
|
10599
|
+
* たまたま一致して見えるが、CDN 一発(`https://esm.run/@wcstack/state/auto`)で
|
|
10600
|
+
* 読み込んだ瞬間に `src="/app.js"` が CDN 側の URL を指して 404 になる。
|
|
10601
|
+
*
|
|
10602
|
+
* `src` は HTML 属性なので、正しい基準は document の base URL である
|
|
10603
|
+
* (`src="*.json"` 側は `fetch` がそう解決しており、同じ属性が形式によって
|
|
10604
|
+
* 違う基準で解決されていた)。絶対 URL・`data:`・`blob:` は URL 解決で
|
|
10605
|
+
* そのまま素通りするため、既存の使い方は影響を受けない。
|
|
10606
|
+
*/
|
|
10607
|
+
function resolveAgainstDocument(url) {
|
|
10608
|
+
return new URL(url, document.baseURI).href;
|
|
10609
|
+
}
|
|
9649
10610
|
async function loadFromScriptFile(url) {
|
|
9650
10611
|
try {
|
|
9651
|
-
const module = await import(/* @vite-ignore */ url);
|
|
10612
|
+
const module = await import(/* @vite-ignore */ resolveAgainstDocument(url));
|
|
9652
10613
|
return module.default || {};
|
|
9653
10614
|
}
|
|
9654
10615
|
catch (e) {
|
|
@@ -10988,7 +11949,9 @@ function processWatchDeclaration(stateElement, state) {
|
|
|
10988
11949
|
paths.add(path);
|
|
10989
11950
|
// 依存グラフ登録(§8)。"for" 以外の bindingType は親 → 子の staticDependency
|
|
10990
11951
|
// チェーンを生やすだけで listPaths / elementPaths を触らない(State.setPathInfo 参照)。
|
|
10991
|
-
|
|
11952
|
+
// source="watch" は存在検査の診断 code を `wcs/watch-path-missing` に切り替える
|
|
11953
|
+
// (watch キーの miss は raiseError にも掛からず、黙って発火しないだけになる)。
|
|
11954
|
+
stateElement.setPathInfo(path, "prop", "watch");
|
|
10992
11955
|
}
|
|
10993
11956
|
setWatchEntries(stateElement, entries);
|
|
10994
11957
|
return paths.size > 0 ? paths : null;
|
|
@@ -11579,11 +12542,18 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
11579
12542
|
if (!tagName.includes("-")) {
|
|
11580
12543
|
raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
|
|
11581
12544
|
}
|
|
11582
|
-
|
|
12545
|
+
// 定義先は「定義元ホストを支配するレジストリ」。scoped registry を持つツリーで
|
|
12546
|
+
// global に define すると、その定義は自分の兄弟にすら適用されない。
|
|
12547
|
+
const definitionRegistry = getCustomElementRegistry(hostElement);
|
|
12548
|
+
if (definitionRegistry === null || typeof definitionRegistry.define !== "function") {
|
|
12549
|
+
raiseError(`DCC: CustomElementRegistry is unavailable for "${tagName}".`);
|
|
12550
|
+
}
|
|
12551
|
+
if (definitionRegistry.get(tagName)) {
|
|
11583
12552
|
// 重複定義は authoring error として落とす。従来は warn してスキップしていたが、
|
|
11584
12553
|
// 先勝ちで別テンプレートのインスタンスが生えるため「動いているように見えて中身が違う」
|
|
11585
12554
|
// 状態になる。state 名の重複(stateElementByName)が raiseError なのと作法を揃える
|
|
11586
12555
|
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §3.4)。
|
|
12556
|
+
// 一意性はレジストリ単位なので、別スコープの同名 DCC は衝突しない。
|
|
11587
12557
|
raiseError(`DCC: "${tagName}" is already registered. A custom element name can only be defined once.`);
|
|
11588
12558
|
}
|
|
11589
12559
|
// ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
|
|
@@ -11638,7 +12608,7 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
11638
12608
|
// カスタム要素として upgrade されていない。ホストが接続済みなら appendChild の時点で
|
|
11639
12609
|
// upgrade されるが、未接続の shadow に挿した場合は upgrade 契機が無く、内側の
|
|
11640
12610
|
// <wcs-state> が素の HTMLElement のまま残って createState が生えない。明示的に upgrade する。
|
|
11641
|
-
const registry = getCustomElementRegistry();
|
|
12611
|
+
const registry = getCustomElementRegistry(this._shadow);
|
|
11642
12612
|
if (registry !== null) {
|
|
11643
12613
|
upgradeCustomElement(registry, this._shadow);
|
|
11644
12614
|
}
|
|
@@ -11705,7 +12675,7 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
11705
12675
|
});
|
|
11706
12676
|
}
|
|
11707
12677
|
// カスタム要素登録
|
|
11708
|
-
|
|
12678
|
+
definitionRegistry.define(tagName, DCCElement);
|
|
11709
12679
|
}
|
|
11710
12680
|
|
|
11711
12681
|
/**
|
|
@@ -11852,6 +12822,41 @@ function disconnectedCallback(target, _prop, receiver, _handler) {
|
|
|
11852
12822
|
}
|
|
11853
12823
|
}
|
|
11854
12824
|
|
|
12825
|
+
/**
|
|
12826
|
+
* getContextListIndex.ts
|
|
12827
|
+
*
|
|
12828
|
+
* Stateの内部APIとして、現在のプロパティ参照スコープにおける
|
|
12829
|
+
* 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
|
|
12830
|
+
* リストインデックス(IListIndex)を取得する関数です。
|
|
12831
|
+
*
|
|
12832
|
+
* 主な役割:
|
|
12833
|
+
* - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
|
|
12834
|
+
* - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
|
|
12835
|
+
*
|
|
12836
|
+
* 設計ポイント:
|
|
12837
|
+
* - 直近のプロパティ参照情報を取得
|
|
12838
|
+
* - info.indexByWildcardPathからstructuredPathのインデックスを特定
|
|
12839
|
+
* - listIndex.at(index)で該当階層のリストインデックスを取得
|
|
12840
|
+
* - パスが一致しない場合や参照が存在しない場合はnullを返す
|
|
12841
|
+
*/
|
|
12842
|
+
function getContextListIndex(handler, structuredPath) {
|
|
12843
|
+
if (handler.addressStackLength === 0) {
|
|
12844
|
+
return null;
|
|
12845
|
+
}
|
|
12846
|
+
const address = handler.lastAddressStack;
|
|
12847
|
+
if (address === null) {
|
|
12848
|
+
return null;
|
|
12849
|
+
}
|
|
12850
|
+
const index = address.pathInfo.indexByWildcardPath[structuredPath];
|
|
12851
|
+
if (typeof index === "undefined") {
|
|
12852
|
+
return null;
|
|
12853
|
+
}
|
|
12854
|
+
if (address.listIndex === null) {
|
|
12855
|
+
return null;
|
|
12856
|
+
}
|
|
12857
|
+
return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
|
|
12858
|
+
}
|
|
12859
|
+
|
|
11855
12860
|
const cacheEntryByAbsoluteStateAddress = new WeakMap();
|
|
11856
12861
|
function getCacheEntryByAbsoluteStateAddress(address) {
|
|
11857
12862
|
return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
|
|
@@ -12077,7 +13082,10 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
|
12077
13082
|
}
|
|
12078
13083
|
}
|
|
12079
13084
|
else {
|
|
12080
|
-
|
|
13085
|
+
// 親アドレスが無い = 単一セグメントのパスが state に存在しない。ここは元から
|
|
13086
|
+
// throw していたが、文面が内部実装の言葉だったので打ち間違いだと分からなかった。
|
|
13087
|
+
// 深いパスの console.warn(pathDiagnostics.checkDeclaredPath)と語彙を揃える。
|
|
13088
|
+
const parentAddress = address.parentAddress ?? raiseError(missingRootPathMessage(stateElement.name, address.pathInfo.path, target, stateElement.getterPaths));
|
|
12081
13089
|
const parentValue = getByAddress(target, parentAddress, receiver, handler);
|
|
12082
13090
|
// 親が居ないパスの読みは undefined(=「state に意見が無い」)。`Reflect.get` に
|
|
12083
13091
|
// そのまま渡すと生の `TypeError: Reflect.get called on non-object` になり、
|
|
@@ -12095,7 +13103,11 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
|
12095
13103
|
}
|
|
12096
13104
|
const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
|
|
12097
13105
|
if (lastSegment === WILDCARD) {
|
|
12098
|
-
|
|
13106
|
+
// listIndex が無いまま末尾ワイルドカードに到達 = そのパスの階数を満たす
|
|
13107
|
+
// ループ文脈が無い(`matrix.*.*` を 1 段の `for` の中で読む等)。元の文面は
|
|
13108
|
+
// 内部の言葉(address.listIndex?.index is undefined)で、何段必要なのかが
|
|
13109
|
+
// 書かれていなかった(pathDiagnostics.ts)。
|
|
13110
|
+
const index = address.listIndex?.index ?? raiseError(wildcardScopeMessage(`path "${address.pathInfo.path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
|
|
12099
13111
|
return Reflect.get(parentValue, index);
|
|
12100
13112
|
}
|
|
12101
13113
|
else {
|
|
@@ -12132,38 +13144,112 @@ function getByAddress(target, address, receiver, handler) {
|
|
|
12132
13144
|
}
|
|
12133
13145
|
|
|
12134
13146
|
/**
|
|
12135
|
-
*
|
|
13147
|
+
* wildcardIndexes.ts
|
|
12136
13148
|
*
|
|
12137
|
-
*
|
|
12138
|
-
*
|
|
12139
|
-
*
|
|
13149
|
+
* ワイルドカードを含むパスから「解決済み添字タプルの集合」を列挙する共有走査。
|
|
13150
|
+
* `$getAll`(読み)と `$setAll`(書き)が**同じ展開規則・同じ順序**で動くための単一の正本
|
|
13151
|
+
* (docs/state-set-all-design.md §6-1)。
|
|
12140
13152
|
*
|
|
12141
|
-
*
|
|
12142
|
-
*
|
|
12143
|
-
*
|
|
13153
|
+
* 添字は**前方一致の接頭辞**で、足りない分は「その階層を全部展開する」という意味を持つ
|
|
13154
|
+
* (README の `$getAll("scores.*", [])` がこれ)。返るタプルは常にワイルドカードの本数と
|
|
13155
|
+
* 同じ長さになるので、そのまま `$resolve` の厳密一致な添字として使える。
|
|
12144
13156
|
*
|
|
12145
|
-
*
|
|
12146
|
-
*
|
|
12147
|
-
*
|
|
12148
|
-
*
|
|
12149
|
-
* -
|
|
13157
|
+
* 順序は**深さ優先・添字昇順**(ネストは添字タプルの辞書順)で決定的。
|
|
13158
|
+
* `$getAll(p, i)` の戻り順と `$setAll(p, i, …)` の適用順が一致する根拠がこれであり、
|
|
13159
|
+
* `$setAll` の `{ spread: true }` 形はこの順序に乗っている。
|
|
13160
|
+
*
|
|
13161
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
|
|
12150
13162
|
*/
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
13163
|
+
/**
|
|
13164
|
+
* 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
|
|
13165
|
+
* ListIndex の同一性を跨いで保つために使う。
|
|
13166
|
+
*
|
|
13167
|
+
* 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
|
|
13168
|
+
* 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
|
|
13169
|
+
*/
|
|
13170
|
+
// ToDo: IAbsoluteStateAddressに変更する
|
|
13171
|
+
const lastValueByListAddress = new WeakMap();
|
|
13172
|
+
/**
|
|
13173
|
+
* `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
|
|
13174
|
+
* マッチする添字タプルを列挙する。
|
|
13175
|
+
*
|
|
13176
|
+
* 添字の本数検査(上限)は呼び出し側の責務 — API 名を診断メッセージに出すため。
|
|
13177
|
+
*/
|
|
13178
|
+
function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, options) {
|
|
13179
|
+
const newValueByAddress = new Map();
|
|
13180
|
+
const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
|
|
13181
|
+
const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
|
|
13182
|
+
if (wildcardParentPathInfo === null) {
|
|
13183
|
+
results.push(parentIndexes);
|
|
13184
|
+
return;
|
|
13185
|
+
}
|
|
13186
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13187
|
+
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13188
|
+
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13189
|
+
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13190
|
+
const listIndexes = listDiff.newIndexes;
|
|
13191
|
+
const index = indexes[indexPos] ?? null;
|
|
13192
|
+
newValueByAddress.set(wildcardAddress, newValue);
|
|
13193
|
+
if (index === null) {
|
|
13194
|
+
for (let i = 0; i < listIndexes.length; i++) {
|
|
13195
|
+
const listIndex = listIndexes[i];
|
|
13196
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
else {
|
|
13200
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13201
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13202
|
+
const listIndex = listIndexes[index] ??
|
|
13203
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13204
|
+
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13205
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13206
|
+
}
|
|
13207
|
+
else {
|
|
13208
|
+
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13209
|
+
results.push(parentIndexes.concat(listIndex.index));
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
13212
|
+
};
|
|
13213
|
+
const resultIndexes = [];
|
|
13214
|
+
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
13215
|
+
if (options.commitDiffBaseline) {
|
|
13216
|
+
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13217
|
+
lastValueByListAddress.set(address, newValue);
|
|
13218
|
+
}
|
|
12165
13219
|
}
|
|
12166
|
-
return
|
|
13220
|
+
return resultIndexes;
|
|
13221
|
+
}
|
|
13222
|
+
|
|
13223
|
+
/**
|
|
13224
|
+
* getListIndexByIndexes.ts
|
|
13225
|
+
*
|
|
13226
|
+
* 解決済みの添字タプル(ワイルドカード 1 段につき 1 個)から、対応する ListIndex を
|
|
13227
|
+
* **正本レジストリ**(listIndexesByList)経由で引き当てる。
|
|
13228
|
+
*
|
|
13229
|
+
* `$resolve` と `$setAll` の共有部分。列挙側(wildcardIndexes.ts)が走査中に生成した
|
|
13230
|
+
* ListIndex をそのまま書き込み先にせず、ここで引き直すことで、binding が使っている
|
|
13231
|
+
* ListIndex と同一の同一性に載る(docs/state-set-all-design.md §6-2)。
|
|
13232
|
+
*
|
|
13233
|
+
* 添字の本数がワイルドカードの本数と一致していることは呼び出し側の責務。
|
|
13234
|
+
*/
|
|
13235
|
+
function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
|
|
13236
|
+
// ワイルドカード階層ごとにListIndexを解決していく
|
|
13237
|
+
let listIndex = null;
|
|
13238
|
+
for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
|
|
13239
|
+
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13240
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13241
|
+
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13242
|
+
const listIndexes = getListIndexesByList(tmpValue);
|
|
13243
|
+
if (listIndexes == null) {
|
|
13244
|
+
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13245
|
+
}
|
|
13246
|
+
const index = indexes[i];
|
|
13247
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13248
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13249
|
+
listIndex = listIndexes[index] ??
|
|
13250
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13251
|
+
}
|
|
13252
|
+
return listIndex;
|
|
12167
13253
|
}
|
|
12168
13254
|
|
|
12169
13255
|
/**
|
|
@@ -12891,7 +13977,8 @@ function _setByAddress(target, address, absAddress, value, receiver, handler, ke
|
|
|
12891
13977
|
const parentValue = getByAddress(target, parentAddress, receiver, handler);
|
|
12892
13978
|
const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
|
|
12893
13979
|
if (lastSegment === WILDCARD) {
|
|
12894
|
-
|
|
13980
|
+
// 読み取り側(getByAddress)と同じ取り違え。書き込みでも何段必要かを言う。
|
|
13981
|
+
const index = address.listIndex?.index ?? raiseError(wildcardScopeMessage(`path "${address.pathInfo.path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
|
|
12895
13982
|
return Reflect.set(parentValue, index, value);
|
|
12896
13983
|
}
|
|
12897
13984
|
else {
|
|
@@ -13060,7 +14147,9 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
13060
14147
|
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
13061
14148
|
try {
|
|
13062
14149
|
if (key === undefined) {
|
|
13063
|
-
|
|
14150
|
+
// fast path 版の同じ取り違え(末尾ワイルドカードに listIndex が無い)。
|
|
14151
|
+
// 通常経路と同じ語彙で「何段必要か」を言う(pathDiagnostics.ts)。
|
|
14152
|
+
raiseError(wildcardScopeMessage(`path "${path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
|
|
13064
14153
|
}
|
|
13065
14154
|
return Reflect.set(parentValue, key, value);
|
|
13066
14155
|
}
|
|
@@ -13159,25 +14248,15 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13159
14248
|
}
|
|
13160
14249
|
}
|
|
13161
14250
|
}
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
//
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13169
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13170
|
-
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13171
|
-
const listIndexes = getListIndexesByList(tmpValue);
|
|
13172
|
-
if (listIndexes == null) {
|
|
13173
|
-
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13174
|
-
}
|
|
13175
|
-
const index = indexes[i];
|
|
13176
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13177
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13178
|
-
listIndex = listIndexes[index] ??
|
|
13179
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
14251
|
+
// 添字の本数はワイルドカードの本数と**厳密に一致**する必要がある。
|
|
14252
|
+
// 不足は元から throw していたが、超過は黙って無視されていた(余分な要素を
|
|
14253
|
+
// 誰も読まないため)= `$resolve("items.*.price", [row, col])` のような
|
|
14254
|
+
// 「1 本しか無いのに 2 本渡す」取り違えが、間違った値を返したまま通っていた。
|
|
14255
|
+
if (indexes.length !== pathInfo.wildcardParentPathInfos.length) {
|
|
14256
|
+
raiseError(indexArityMessage("$resolve", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
13180
14257
|
}
|
|
14258
|
+
// ワイルドカード階層ごとにListIndexを解決していく(`$setAll` と共有)
|
|
14259
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, indexes);
|
|
13181
14260
|
// ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
|
|
13182
14261
|
const address = createStateAddress(pathInfo, listIndex);
|
|
13183
14262
|
const hasSetValue = typeof value !== "undefined";
|
|
@@ -13194,14 +14273,18 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13194
14273
|
* getAllReadonly
|
|
13195
14274
|
*
|
|
13196
14275
|
* ワイルドカードを含む State パスから、対象となる全要素を配列で取得する。
|
|
13197
|
-
*
|
|
14276
|
+
* 走査そのものは `$setAll` と共有する(wildcardIndexes.ts)。
|
|
14277
|
+
*
|
|
14278
|
+
* `indexes` 省略時の既定はループ文脈の添字 `[$1..$n]`。正確には「path と文脈が
|
|
14279
|
+
* 共有するワイルドカード連鎖の分だけ文脈の添字を接頭辞として敷く」(整合最長接頭辞)。
|
|
14280
|
+
* 共有が無いのに文脈が添字を持つ場合は throw する — 異なる文脈の添字は流用しない。
|
|
14281
|
+
*
|
|
14282
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)、
|
|
14283
|
+
* 添字本数超過(wcs/index-arity)、省略時の文脈不整合(getAllContextMismatchMessage)
|
|
13198
14284
|
*/
|
|
13199
|
-
// ToDo: IAbsoluteStateAddressに変更する
|
|
13200
|
-
const lastValueByListAddress = new WeakMap();
|
|
13201
14285
|
function getAll(target, prop, receiver, handler) {
|
|
13202
14286
|
const resolveFn = resolve(target, prop, receiver, handler);
|
|
13203
14287
|
return (path, indexes) => {
|
|
13204
|
-
const newValueByAddress = new Map();
|
|
13205
14288
|
const pathInfo = getPathInfo(path);
|
|
13206
14289
|
if (handler.addressStackLength > 0) {
|
|
13207
14290
|
const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
|
|
@@ -13213,61 +14296,48 @@ function getAll(target, prop, receiver, handler) {
|
|
|
13213
14296
|
}
|
|
13214
14297
|
}
|
|
13215
14298
|
}
|
|
14299
|
+
// 明示的に渡された添字だけを検査する。`$getAll` の添字は**前方一致の接頭辞**で、
|
|
14300
|
+
// 足りない分は「その階層を全部展開する」という正しい意味を持つ(README の
|
|
14301
|
+
// `$getAll("scores.*", [])` がこれ)。一方**超過は意味を持たず黙って捨てられ**、
|
|
14302
|
+
// ワイルドカードの本数を取り違えたまま部分集合が返っていた。
|
|
14303
|
+
// 省略時に下で導出する添字は文脈由来なので、この検査には掛けない。
|
|
14304
|
+
if (typeof indexes !== "undefined" && indexes.length > pathInfo.wildcardParentPathInfos.length) {
|
|
14305
|
+
raiseError(indexArityMessage("$getAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
14306
|
+
}
|
|
13216
14307
|
if (typeof indexes === "undefined") {
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
14308
|
+
// 省略時の既定はループ文脈の添字 `[$1..$n]`。ただし敷けるのは path と文脈が
|
|
14309
|
+
// **共有するワイルドカード連鎖**の分だけなので、path のワイルドカードを
|
|
14310
|
+
// 内側(最深)から探し、最初に文脈にヒットした階層の scoped indexes を接頭辞にする。
|
|
14311
|
+
// ワイルドカードパスの序数はパス文字列自身の `*` の本数で決まるため、深い側が
|
|
14312
|
+
// ヒットすれば浅い側は必ず含まれ、これが整合する最長の接頭辞になる。文脈が
|
|
14313
|
+
// path より深い分は自然に切り詰められ、導出した接頭辞は path のワイルドカード
|
|
14314
|
+
// 本数を超えないので、上の本数検査には掛けない。
|
|
14315
|
+
for (let i = pathInfo.wildcardPaths.length - 1; i >= 0; i--) {
|
|
14316
|
+
const listIndex = getContextListIndex(handler, pathInfo.wildcardPaths[i]);
|
|
13220
14317
|
if (listIndex) {
|
|
13221
14318
|
indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
|
|
13222
14319
|
break;
|
|
13223
14320
|
}
|
|
13224
14321
|
}
|
|
13225
14322
|
if (typeof indexes === "undefined") {
|
|
14323
|
+
// 共有ゼロ。文脈が自スコープの添字を実際に持っているなら、既定の `[...$n]` は
|
|
14324
|
+
// **異なる文脈の添字の流用(混入)**になるため、黙って全展開へ倒さず throw する。
|
|
14325
|
+
// 文脈そのものが無い(トップレベル getter / メソッド直下)なら全展開が既定。
|
|
14326
|
+
const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
|
|
14327
|
+
const contextListIndex = lastAddress?.listIndex ?? null;
|
|
14328
|
+
if (pathInfo.wildcardCount > 0 && lastAddress !== null && contextListIndex !== null &&
|
|
14329
|
+
contextListIndex.length - getBaseDepth(handler.stateElement) > 0) {
|
|
14330
|
+
raiseError(getAllContextMismatchMessage(path, lastAddress.pathInfo.path));
|
|
14331
|
+
}
|
|
13226
14332
|
indexes = [];
|
|
13227
14333
|
}
|
|
13228
14334
|
}
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
if (wildcardParentPathInfo === null) {
|
|
13232
|
-
results.push(parentIndexes);
|
|
13233
|
-
return;
|
|
13234
|
-
}
|
|
13235
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13236
|
-
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13237
|
-
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13238
|
-
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13239
|
-
const listIndexes = listDiff.newIndexes;
|
|
13240
|
-
const index = indexes[indexPos] ?? null;
|
|
13241
|
-
newValueByAddress.set(wildcardAddress, newValue);
|
|
13242
|
-
if (index === null) {
|
|
13243
|
-
for (let i = 0; i < listIndexes.length; i++) {
|
|
13244
|
-
const listIndex = listIndexes[i];
|
|
13245
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13246
|
-
}
|
|
13247
|
-
}
|
|
13248
|
-
else {
|
|
13249
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13250
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13251
|
-
const listIndex = listIndexes[index] ??
|
|
13252
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13253
|
-
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13254
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13255
|
-
}
|
|
13256
|
-
else {
|
|
13257
|
-
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13258
|
-
results.push(parentIndexes.concat(listIndex.index));
|
|
13259
|
-
}
|
|
13260
|
-
}
|
|
13261
|
-
};
|
|
13262
|
-
const resultIndexes = [];
|
|
13263
|
-
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
14335
|
+
// 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
|
|
14336
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
|
|
13264
14337
|
const resultValues = [];
|
|
13265
14338
|
for (let i = 0; i < resultIndexes.length; i++) {
|
|
13266
14339
|
resultValues.push(resolveFn(pathInfo.path, resultIndexes[i]));
|
|
13267
14340
|
}
|
|
13268
|
-
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13269
|
-
lastValueByListAddress.set(address, newValue);
|
|
13270
|
-
}
|
|
13271
14341
|
return resultValues;
|
|
13272
14342
|
};
|
|
13273
14343
|
}
|
|
@@ -13351,6 +14421,83 @@ function postUpdate(target, _prop, receiver, handler) {
|
|
|
13351
14421
|
};
|
|
13352
14422
|
}
|
|
13353
14423
|
|
|
14424
|
+
/**
|
|
14425
|
+
* setAll.ts
|
|
14426
|
+
*
|
|
14427
|
+
* ワイルドカードを含む State パスにマッチする**全アドレスへ一括で書き込む**。
|
|
14428
|
+
* `$getAll`(読み)の対称形(docs/state-set-all-design.md)。
|
|
14429
|
+
*
|
|
14430
|
+
* 存在理由は糖衣ではなく「**リスト全置換の回避**」(設計 §1-1)。
|
|
14431
|
+
* `this.users = this.users.map(...)` は配列を作り直すので ListIndex・行 getter
|
|
14432
|
+
* キャッシュ・差分描画がまとめて作り直しになる。`$setAll` は意味としては一括更新、
|
|
14433
|
+
* 実体は in-place な個別書き込みで、同じことを差分に載せたまま行う。
|
|
14434
|
+
*
|
|
14435
|
+
* 3 つの形(設計 §2):
|
|
14436
|
+
* - ブロードキャスト `$setAll(path, indexes, value)`
|
|
14437
|
+
* - mapper(第一級) `$setAll(path, indexes, (current, ...indexes) => next)`
|
|
14438
|
+
* - spread `$setAll(path, indexes, values, { spread: true })`
|
|
14439
|
+
*/
|
|
14440
|
+
function setAll(target, _prop, receiver, handler) {
|
|
14441
|
+
return (path, indexes, value, options) => {
|
|
14442
|
+
const pathInfo = getPathInfo(path);
|
|
14443
|
+
// 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
|
|
14444
|
+
// 「現在行」ではなく「全行」を意味する(設計 §4-1)。
|
|
14445
|
+
if (!Array.isArray(indexes)) {
|
|
14446
|
+
raiseError(setAllValueKindMessage(path, "requires an explicit indexes array (pass [] to expand every level)."));
|
|
14447
|
+
}
|
|
14448
|
+
// 添字は前方一致の接頭辞なので不足は正当。超過だけを弾く(`$getAll` と同じ規則)。
|
|
14449
|
+
if (indexes.length > pathInfo.wildcardParentPathInfos.length) {
|
|
14450
|
+
raiseError(indexArityMessage("$setAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
14451
|
+
}
|
|
14452
|
+
const spread = options?.spread === true;
|
|
14453
|
+
const isMapper = typeof value === "function";
|
|
14454
|
+
if (spread && isMapper) {
|
|
14455
|
+
raiseError(setAllValueKindMessage(path, "cannot combine { spread: true } with a mapper function."));
|
|
14456
|
+
}
|
|
14457
|
+
if (spread && !Array.isArray(value)) {
|
|
14458
|
+
raiseError(setAllValueKindMessage(path, "requires an array as the value when { spread: true } is set."));
|
|
14459
|
+
}
|
|
14460
|
+
// --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
|
|
14461
|
+
// 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
|
|
14462
|
+
// 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
|
|
14463
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
|
|
14464
|
+
if (spread && value.length !== resultIndexes.length) {
|
|
14465
|
+
raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
|
|
14466
|
+
}
|
|
14467
|
+
const addresses = [];
|
|
14468
|
+
for (let i = 0; i < resultIndexes.length; i++) {
|
|
14469
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, resultIndexes[i]);
|
|
14470
|
+
addresses.push(createStateAddress(pathInfo, listIndex));
|
|
14471
|
+
}
|
|
14472
|
+
// --- 第 2 相: 確定したアドレスにだけ書く ---
|
|
14473
|
+
let written = 0;
|
|
14474
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
14475
|
+
const address = addresses[i];
|
|
14476
|
+
let nextValue;
|
|
14477
|
+
if (isMapper) {
|
|
14478
|
+
// 現在値は書く直前に読む。先行する書き込みが getter 経由で他行に及ぶ場合、
|
|
14479
|
+
// mapper が見るべきなのは最新値。
|
|
14480
|
+
const currentValue = getByAddress(target, address, receiver, handler);
|
|
14481
|
+
nextValue = value(currentValue, ...resultIndexes[i]);
|
|
14482
|
+
}
|
|
14483
|
+
else if (spread) {
|
|
14484
|
+
nextValue = value[i];
|
|
14485
|
+
}
|
|
14486
|
+
else {
|
|
14487
|
+
nextValue = value;
|
|
14488
|
+
}
|
|
14489
|
+
// undefined は常にスキップ(設計 §5)。mapper の return 忘れで全行を潰さないため、
|
|
14490
|
+
// かつ「この行は変えない」を表現できるようにするため。クリアは null。
|
|
14491
|
+
if (typeof nextValue === "undefined") {
|
|
14492
|
+
continue;
|
|
14493
|
+
}
|
|
14494
|
+
setByAddress(target, address, nextValue, receiver, handler);
|
|
14495
|
+
written++;
|
|
14496
|
+
}
|
|
14497
|
+
return written;
|
|
14498
|
+
};
|
|
14499
|
+
}
|
|
14500
|
+
|
|
13354
14501
|
/**
|
|
13355
14502
|
* trackDependency.ts
|
|
13356
14503
|
*
|
|
@@ -13525,7 +14672,7 @@ function setLoopContext(handler, loopContext, callback) {
|
|
|
13525
14672
|
* StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
|
|
13526
14673
|
*
|
|
13527
14674
|
* 主な役割:
|
|
13528
|
-
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
|
|
14675
|
+
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $setAll, $postUpdate,
|
|
13529
14676
|
* $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
|
|
13530
14677
|
* - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
|
|
13531
14678
|
* - getByRefで構造化パス・リストインデックスに対応した値を取得
|
|
@@ -13588,6 +14735,11 @@ function get(target, prop, receiver, handler) {
|
|
|
13588
14735
|
return getAll(target, prop, receiver, handler)(path, indexes);
|
|
13589
14736
|
};
|
|
13590
14737
|
}
|
|
14738
|
+
case "$setAll": {
|
|
14739
|
+
return (path, indexes, value, options) => {
|
|
14740
|
+
return setAll(target, prop, receiver, handler)(path, indexes, value, options);
|
|
14741
|
+
};
|
|
14742
|
+
}
|
|
13591
14743
|
case "$postUpdate": {
|
|
13592
14744
|
return (path) => {
|
|
13593
14745
|
return postUpdate(target, prop, receiver, handler)(path);
|
|
@@ -13719,6 +14871,8 @@ function set(target, prop, value, receiver, handler) {
|
|
|
13719
14871
|
}
|
|
13720
14872
|
}
|
|
13721
14873
|
|
|
14874
|
+
/** 循環報告に載せるスタック末尾の段数(当事者が見える最小限) */
|
|
14875
|
+
const CYCLE_REPORT_DEPTH = 8;
|
|
13722
14876
|
class StateHandler {
|
|
13723
14877
|
_stateElement;
|
|
13724
14878
|
_stateName;
|
|
@@ -13759,12 +14913,36 @@ class StateHandler {
|
|
|
13759
14913
|
return this._loopContext;
|
|
13760
14914
|
}
|
|
13761
14915
|
pushAddress(address) {
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
14916
|
+
// 上限判定は **increment より前**に行う。後にすると、深さ超過で throw した時点で
|
|
14917
|
+
// `_addressStackIndex` だけが進み `_addressStack[index]` は未代入のまま残る。
|
|
14918
|
+
// 呼び出し側(getByAddress)は `pushAddress` を try の外で呼ぶので自分では pop
|
|
14919
|
+
// しないが、外側フレームの finally が順に pop していき、その 1 本目が未代入の枠を
|
|
14920
|
+
// 引いて `Address stack at index N is undefined.` を投げる = **本来の
|
|
14921
|
+
// 「無限ループの疑い」という診断が巻き戻しの最中に上書きされて消える**。
|
|
14922
|
+
// getter の相互参照(`get a(){return this.b}` / `get b(){return this.a}`)は
|
|
14923
|
+
// 実際にこれを踏み、原因と無関係な文面だけが残っていた。
|
|
14924
|
+
if (this._addressStackIndex + 1 >= MAX_LOOP_DEPTH) {
|
|
14925
|
+
raiseError(`Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH}. ` +
|
|
14926
|
+
`Possible circular dependency between path getters: ${this._describeAddressCycle()}`);
|
|
13765
14927
|
}
|
|
14928
|
+
this._addressStackIndex++;
|
|
13766
14929
|
this._addressStack[this._addressStackIndex] = address;
|
|
13767
14930
|
}
|
|
14931
|
+
/**
|
|
14932
|
+
* スタック末尾の繰り返し区間をパス名で示す(循環の当事者だけを見せる)。
|
|
14933
|
+
* 上限に達したときのみ呼ばれるので、コストは異常系に閉じている。
|
|
14934
|
+
*/
|
|
14935
|
+
_describeAddressCycle() {
|
|
14936
|
+
const paths = [];
|
|
14937
|
+
for (let i = this._addressStackIndex; i >= 0 && paths.length < CYCLE_REPORT_DEPTH; i--) {
|
|
14938
|
+
const entry = this._addressStack[i];
|
|
14939
|
+
if (entry) {
|
|
14940
|
+
paths.push(entry.pathInfo.path);
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
const unique = Array.from(new Set(paths));
|
|
14944
|
+
return `${unique.reverse().join(" -> ")} -> ...`;
|
|
14945
|
+
}
|
|
13768
14946
|
popAddress() {
|
|
13769
14947
|
if (this._addressStackIndex < 0) {
|
|
13770
14948
|
return null;
|
|
@@ -14353,7 +15531,13 @@ class State extends HTMLElementBase {
|
|
|
14353
15531
|
raiseError(`"bind-component" cannot be combined with ${conflicting.join(", ")}. The component's "${this.getAttribute("bind-component")}" property is the only state source.`);
|
|
14354
15532
|
}
|
|
14355
15533
|
const boundComponentStateProp = this.getAttribute("bind-component");
|
|
14356
|
-
|
|
15534
|
+
const componentRegistry = getCustomElementRegistry(boundComponent);
|
|
15535
|
+
if (componentRegistry === null) {
|
|
15536
|
+
// null レジストリのサブツリーではホストは永久に upgrade されない。
|
|
15537
|
+
// whenDefined を待つと無言でウェッジするので落とす。
|
|
15538
|
+
raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
|
|
15539
|
+
}
|
|
15540
|
+
await componentRegistry.whenDefined(customTagName.toLowerCase());
|
|
14357
15541
|
// data-wcs属性がある場合は、上位の状態によりbinding情報の設定が完了するまで待機する
|
|
14358
15542
|
if (boundComponent.hasAttribute(config.bindAttributeName)) {
|
|
14359
15543
|
await waitInitializeBinding(boundComponent);
|
|
@@ -14518,8 +15702,12 @@ class State extends HTMLElementBase {
|
|
|
14518
15702
|
if (!this.hasAttribute('enable-ssr') || inSsr()) {
|
|
14519
15703
|
await this._callStateConnectedCallback();
|
|
14520
15704
|
}
|
|
14521
|
-
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr>
|
|
14522
|
-
|
|
15705
|
+
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
|
|
15706
|
+
// orchestrated(サーバー主導の最終パス、docs/ssr-router-design.md §5)では
|
|
15707
|
+
// 生成しない — renderToString が全要素の完了後にまとめて生成するため。
|
|
15708
|
+
// ここで生成すると、router 等が後から挿入した内容の構造テンプレートを
|
|
15709
|
+
// 取り逃がすレースがある(state のロード方式と文書順に依存)
|
|
15710
|
+
if (inSsr() && this.hasAttribute('enable-ssr') && !isOrchestratedSsr()) {
|
|
14523
15711
|
try {
|
|
14524
15712
|
await getBindingsReady(this.rootNode);
|
|
14525
15713
|
const name = this.getAttribute('name') || 'default';
|
|
@@ -14727,7 +15915,7 @@ class State extends HTMLElementBase {
|
|
|
14727
15915
|
addStaticDependency(sourcePath, targetPath) {
|
|
14728
15916
|
return this._addDependency(this._staticDependency, sourcePath, targetPath);
|
|
14729
15917
|
}
|
|
14730
|
-
setPathInfo(path, bindingType) {
|
|
15918
|
+
setPathInfo(path, bindingType, source = "binding") {
|
|
14731
15919
|
if (bindingType === "for") {
|
|
14732
15920
|
const isNewListPath = !this._listPaths.has(path);
|
|
14733
15921
|
this._listPaths.add(path);
|
|
@@ -14742,6 +15930,10 @@ class State extends HTMLElementBase {
|
|
|
14742
15930
|
if (!this._pathSet.has(path)) {
|
|
14743
15931
|
const pathInfo = getPathInfo(path);
|
|
14744
15932
|
this._pathSet.add(path);
|
|
15933
|
+
// 存在しないパスへの配線は「黙って更新されない」だけで終わるため、
|
|
15934
|
+
// 新規パスを 1 回だけ検査して確実な miss を報告する(pathDiagnostics.ts)。
|
|
15935
|
+
// パスごとに 1 回・バインド確立時のみで、更新のホットパスには乗らない。
|
|
15936
|
+
checkDeclaredPath(this, this.__state, path, source);
|
|
14745
15937
|
if (pathInfo.parentPath !== null) {
|
|
14746
15938
|
let currentPathInfo = pathInfo;
|
|
14747
15939
|
while (currentPathInfo.parentPath !== null) {
|
|
@@ -14803,20 +15995,77 @@ class State extends HTMLElementBase {
|
|
|
14803
15995
|
}
|
|
14804
15996
|
}
|
|
14805
15997
|
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
15998
|
+
/**
|
|
15999
|
+
* Register this package's tags. Pass a scoped `CustomElementRegistry` to define
|
|
16000
|
+
* them for a single shadow tree -- scoped registries do not inherit the global
|
|
16001
|
+
* one, so a tree using one needs its own definitions.
|
|
16002
|
+
*/
|
|
16003
|
+
function registerComponents(registry = customElements) {
|
|
16004
|
+
if (!registry.get(config.tagNames.ssr)) {
|
|
16005
|
+
registry.define(config.tagNames.ssr, Ssr);
|
|
14809
16006
|
}
|
|
14810
|
-
if (!
|
|
14811
|
-
|
|
16007
|
+
if (!registry.get(config.tagNames.state)) {
|
|
16008
|
+
registry.define(config.tagNames.state, State);
|
|
14812
16009
|
}
|
|
14813
16010
|
}
|
|
14814
16011
|
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
16012
|
+
/**
|
|
16013
|
+
* `<html lang>` を既定ロケールとして採る。
|
|
16014
|
+
*
|
|
16015
|
+
* ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は `config.locale`
|
|
16016
|
+
* を読むが、それを設定できる公開の入口は `bootstrapState({ locale })` しかない。
|
|
16017
|
+
* 一方 `auto` エントリは `bootstrapState()` を引数なしで呼ぶため、CDN 一発
|
|
16018
|
+
* (`<script src=".../@wcstack/state/auto">`)で読み込んだページには**ロケールを
|
|
16019
|
+
* 渡す口が無かった**。auto バンドルは SRI のため自己完結で、別途 `@wcstack/state`
|
|
16020
|
+
* を import して `bootstrapState` を呼んでも別インスタンスになり効かない。
|
|
16021
|
+
*
|
|
16022
|
+
* `<html lang>` はページのロケールを書く HTML 標準の場所であり、SSR ではサーバーが、
|
|
16023
|
+
* 静的ページでは head のスニペットが DOM 解析前に書く。そこを既定にすると
|
|
16024
|
+
* **ロケールの正本が 1 つになり**、「設定を早く呼ぶ」という守りにくい順序の約束が
|
|
16025
|
+
* 「`<html lang>` が state のロードより前にある」という構造的な保証に変わる。
|
|
16026
|
+
*
|
|
16027
|
+
* 明示指定(`bootstrapState({ locale })`)が常に優先する。
|
|
16028
|
+
*/
|
|
16029
|
+
function localeFromDocument() {
|
|
16030
|
+
const lang = document.documentElement?.lang;
|
|
16031
|
+
if (!lang) {
|
|
16032
|
+
return undefined;
|
|
16033
|
+
}
|
|
16034
|
+
try {
|
|
16035
|
+
// 妥当な BCP-47 タグでなければ Intl が RangeError を投げる。不正な lang を
|
|
16036
|
+
// そのまま採ると、これまで既定 'en' で動いていたページのフィルタが実行時に
|
|
16037
|
+
// 落ちる。既定へ落として警告するほうが、黙って壊すより回復しやすい。
|
|
16038
|
+
Intl.getCanonicalLocales(lang);
|
|
16039
|
+
return lang;
|
|
16040
|
+
}
|
|
16041
|
+
catch {
|
|
16042
|
+
console.warn(`[@wcstack/state] <html lang="${lang}"> is not a valid BCP-47 language tag. ` +
|
|
16043
|
+
`Falling back to the default locale for filters.`);
|
|
16044
|
+
return undefined;
|
|
16045
|
+
}
|
|
16046
|
+
}
|
|
16047
|
+
function resolveConfig(config) {
|
|
16048
|
+
if (typeof config?.locale === "string") {
|
|
16049
|
+
return config;
|
|
16050
|
+
}
|
|
16051
|
+
const locale = localeFromDocument();
|
|
16052
|
+
if (locale === undefined) {
|
|
16053
|
+
return config;
|
|
16054
|
+
}
|
|
16055
|
+
return { ...config, locale };
|
|
16056
|
+
}
|
|
16057
|
+
function bootstrapState(config, registry) {
|
|
16058
|
+
const resolved = resolveConfig(config);
|
|
16059
|
+
if (resolved) {
|
|
16060
|
+
setConfig(resolved);
|
|
14818
16061
|
}
|
|
14819
|
-
registerComponents();
|
|
16062
|
+
registerComponents(registry);
|
|
16063
|
+
// binder プロトコルの提供(docs/binder-protocol-design.md)。router が後から
|
|
16064
|
+
// 差し込むノードをバインドできるようにする。登録は冪等。
|
|
16065
|
+
registerBinder();
|
|
16066
|
+
// ssr-snapshot プロトコルの提供(docs/ssr-router-design.md §5)。renderToString が
|
|
16067
|
+
// <wcs-ssr> 生成をサーバー主導の最終パスへ回せるようにする。登録は冪等。
|
|
16068
|
+
registerSsrSnapshotBuilder();
|
|
14820
16069
|
// DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
|
|
14821
16070
|
registerDevtoolsSource();
|
|
14822
16071
|
}
|