@wcstack/state 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +22 -4
- package/dist/index.esm.js +1220 -839
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/dist/manifest.esm.js +6 -0
- package/dist/wcs-manifest.json +4 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
let _inSsrCache = null;
|
|
2
1
|
function inSsr() {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
|
|
3
|
+
// 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
|
|
4
|
+
// した後、同一プロセスでクライアント側ハイドレーションが走る(SSR→hydrate の
|
|
5
|
+
// e2e が該当)。サーバーフェーズの判定をキャッシュするとクライアントフェーズが
|
|
6
|
+
// SSR モード扱いになり、hydrateBindings の代わりに buildBindings が選ばれて
|
|
7
|
+
// connectedCallbackPromise が永久に未解決になる。
|
|
8
|
+
const html = document.documentElement;
|
|
9
|
+
return html ? html.hasAttribute('data-wcs-server') : false;
|
|
8
10
|
}
|
|
9
11
|
const _config = {
|
|
10
12
|
bindAttributeName: 'data-wcs',
|
|
@@ -63,16 +65,6 @@ function setConfig(partialConfig) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
var version$1 = "1.19.0";
|
|
67
|
-
var pkg = {
|
|
68
|
-
version: version$1};
|
|
69
|
-
|
|
70
|
-
const VERSION = pkg.version;
|
|
71
|
-
|
|
72
|
-
function raiseError(message) {
|
|
73
|
-
throw new Error(`[@wcstack/state] ${message}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
68
|
const bindingPromiseByNode = new WeakMap();
|
|
77
69
|
let id$1 = 0;
|
|
78
70
|
function getInitializeBindingPromiseByNode(node) {
|
|
@@ -101,6 +93,10 @@ function resolveInitializedBinding(node) {
|
|
|
101
93
|
bindingPromise.resolve();
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
function raiseError(message) {
|
|
97
|
+
throw new Error(`[@wcstack/state] ${message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
104
100
|
function replaceToReplaceNode(bindingInfo) {
|
|
105
101
|
const node = bindingInfo.node;
|
|
106
102
|
const replaceNode = bindingInfo.replaceNode;
|
|
@@ -1591,6 +1587,37 @@ function parseBindTextForEmbeddedNode(bindText) {
|
|
|
1591
1587
|
};
|
|
1592
1588
|
}
|
|
1593
1589
|
|
|
1590
|
+
const fragmentInfoByUUID = new Map();
|
|
1591
|
+
function setFragmentInfoByUUID(uuid, rootNode, fragmentInfo) {
|
|
1592
|
+
if (fragmentInfo === null) {
|
|
1593
|
+
fragmentInfoByUUID.delete(uuid);
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
fragmentInfoByUUID.set(uuid, fragmentInfo);
|
|
1597
|
+
const bindingPartial = fragmentInfo.parseBindTextResult;
|
|
1598
|
+
const stateElement = getStateElementByName(rootNode, bindingPartial.stateName);
|
|
1599
|
+
if (stateElement === null) {
|
|
1600
|
+
raiseError(`State element with name "${bindingPartial.stateName}" not found for fragment info.`);
|
|
1601
|
+
}
|
|
1602
|
+
stateElement.setPathInfo(bindingPartial.statePathName, bindingPartial.bindingType);
|
|
1603
|
+
for (const nodeInfo of fragmentInfo.nodeInfos) {
|
|
1604
|
+
for (const nodeBindingPartial of nodeInfo.parseBindTextResults) {
|
|
1605
|
+
const nodeStateElement = getStateElementByName(rootNode, nodeBindingPartial.stateName);
|
|
1606
|
+
if (nodeStateElement === null) {
|
|
1607
|
+
raiseError(`State element with name "${nodeBindingPartial.stateName}" not found for fragment info node.`);
|
|
1608
|
+
}
|
|
1609
|
+
nodeStateElement.setPathInfo(nodeBindingPartial.statePathName, nodeBindingPartial.bindingType);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function getFragmentInfoByUUID(uuid) {
|
|
1615
|
+
return fragmentInfoByUUID.get(uuid) || null;
|
|
1616
|
+
}
|
|
1617
|
+
function getAllFragmentUUIDs() {
|
|
1618
|
+
return Array.from(fragmentInfoByUUID.keys());
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1594
1621
|
function getParseBindTextResults(node) {
|
|
1595
1622
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1596
1623
|
const element = node;
|
|
@@ -1773,6 +1800,23 @@ function createStateAddress(pathInfo, listIndex) {
|
|
|
1773
1800
|
}
|
|
1774
1801
|
}
|
|
1775
1802
|
|
|
1803
|
+
/**
|
|
1804
|
+
* devtools/sink.ts
|
|
1805
|
+
*
|
|
1806
|
+
* 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
|
|
1807
|
+
* 計装される側(stateElementByName / setByAddress / binding / token)と
|
|
1808
|
+
* bridge の間の循環 import を避ける。
|
|
1809
|
+
*
|
|
1810
|
+
* コスト規範(protocol §1-1): フック未接続時、計装点のコストは
|
|
1811
|
+
* `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
|
|
1812
|
+
* 必ずこのチェックの内側で行うこと。
|
|
1813
|
+
*/
|
|
1814
|
+
/** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
|
|
1815
|
+
let devtoolsSink = null;
|
|
1816
|
+
function setDevtoolsSink(sink) {
|
|
1817
|
+
devtoolsSink = sink;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1776
1820
|
// command-token / event-token が共有する pub/sub プリミティブ。
|
|
1777
1821
|
// _subscribers は Set のため挿入順を保持する。
|
|
1778
1822
|
// emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
|
|
@@ -1812,7 +1856,31 @@ class Token {
|
|
|
1812
1856
|
|
|
1813
1857
|
// CommandToken は共有 pub/sub プリミティブ Token の薄い特化。
|
|
1814
1858
|
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
1859
|
+
//
|
|
1860
|
+
// ownerStateName は devtools 計装(protocol §4.5)のための内部 optional 引数。
|
|
1861
|
+
// command-token-protocol の外部仕様は不変更(registry が渡すだけで、
|
|
1862
|
+
// subscribe/emit の意味論には一切影響しない)。
|
|
1815
1863
|
class CommandToken extends Token {
|
|
1864
|
+
_ownerStateName;
|
|
1865
|
+
constructor(name, ownerStateName) {
|
|
1866
|
+
super(name);
|
|
1867
|
+
this._ownerStateName = ownerStateName ?? null;
|
|
1868
|
+
}
|
|
1869
|
+
emit(...args) {
|
|
1870
|
+
if (devtoolsSink !== null) {
|
|
1871
|
+
// subscriberCount 0 の emit(空撃ち)もそのまま流す — whenDefined 前の
|
|
1872
|
+
// command 空撃ちレース類をタイムラインで可視化するため
|
|
1873
|
+
devtoolsSink({
|
|
1874
|
+
type: "state:token-emit",
|
|
1875
|
+
kind: "command",
|
|
1876
|
+
stateName: this._ownerStateName,
|
|
1877
|
+
tokenName: this.name,
|
|
1878
|
+
args,
|
|
1879
|
+
subscriberCount: this.size,
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
return super.emit(...args);
|
|
1883
|
+
}
|
|
1816
1884
|
}
|
|
1817
1885
|
function isCommandToken(value) {
|
|
1818
1886
|
return value instanceof CommandToken;
|
|
@@ -1956,7 +2024,28 @@ function attachEventHandler(binding) {
|
|
|
1956
2024
|
|
|
1957
2025
|
// EventToken は共有 pub/sub プリミティブ Token の薄い特化(element→state 方向)。
|
|
1958
2026
|
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
2027
|
+
//
|
|
2028
|
+
// ownerStateName は devtools 計装(protocol §4.5)のための内部 optional 引数。
|
|
2029
|
+
// event-token-protocol の外部仕様は不変更。
|
|
1959
2030
|
class EventToken extends Token {
|
|
2031
|
+
_ownerStateName;
|
|
2032
|
+
constructor(name, ownerStateName) {
|
|
2033
|
+
super(name);
|
|
2034
|
+
this._ownerStateName = ownerStateName ?? null;
|
|
2035
|
+
}
|
|
2036
|
+
emit(...args) {
|
|
2037
|
+
if (devtoolsSink !== null) {
|
|
2038
|
+
devtoolsSink({
|
|
2039
|
+
type: "state:token-emit",
|
|
2040
|
+
kind: "event",
|
|
2041
|
+
stateName: this._ownerStateName,
|
|
2042
|
+
tokenName: this.name,
|
|
2043
|
+
args,
|
|
2044
|
+
subscriberCount: this.size,
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
return super.emit(...args);
|
|
2048
|
+
}
|
|
1960
2049
|
}
|
|
1961
2050
|
|
|
1962
2051
|
const registryByStateElement$2 = new WeakMap();
|
|
@@ -1968,7 +2057,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
1968
2057
|
}
|
|
1969
2058
|
let token = registry.get(name);
|
|
1970
2059
|
if (typeof token === "undefined") {
|
|
1971
|
-
token = new EventToken(name);
|
|
2060
|
+
token = new EventToken(name, stateElement.name);
|
|
1972
2061
|
registry.set(name, token);
|
|
1973
2062
|
}
|
|
1974
2063
|
return token;
|
|
@@ -2604,7 +2693,7 @@ function getUUID() {
|
|
|
2604
2693
|
return `u${(count++).toString(36)}`;
|
|
2605
2694
|
}
|
|
2606
2695
|
|
|
2607
|
-
let version = 0;
|
|
2696
|
+
let version$1 = 0;
|
|
2608
2697
|
class ListIndex {
|
|
2609
2698
|
uuid = getUUID();
|
|
2610
2699
|
parentListIndex;
|
|
@@ -2625,7 +2714,7 @@ class ListIndex {
|
|
|
2625
2714
|
this.position = parentListIndex ? parentListIndex.position + 1 : 0;
|
|
2626
2715
|
this.length = this.position + 1;
|
|
2627
2716
|
this._index = index;
|
|
2628
|
-
this._version = version;
|
|
2717
|
+
this._version = version$1;
|
|
2629
2718
|
}
|
|
2630
2719
|
/**
|
|
2631
2720
|
* Gets current index value.
|
|
@@ -2642,7 +2731,7 @@ class ListIndex {
|
|
|
2642
2731
|
*/
|
|
2643
2732
|
set index(value) {
|
|
2644
2733
|
this._index = value;
|
|
2645
|
-
this._version = ++version;
|
|
2734
|
+
this._version = ++version$1;
|
|
2646
2735
|
this.indexes[this.position] = value;
|
|
2647
2736
|
}
|
|
2648
2737
|
/**
|
|
@@ -2681,7 +2770,7 @@ class ListIndex {
|
|
|
2681
2770
|
else {
|
|
2682
2771
|
if (typeof this._indexes === "undefined" || this.dirty) {
|
|
2683
2772
|
this._indexes = [...this.parentListIndex.indexes, this._index];
|
|
2684
|
-
this._version = version;
|
|
2773
|
+
this._version = version$1;
|
|
2685
2774
|
}
|
|
2686
2775
|
}
|
|
2687
2776
|
return this._indexes;
|
|
@@ -2788,6 +2877,20 @@ function isSameList(oldList, newList) {
|
|
|
2788
2877
|
}
|
|
2789
2878
|
return true;
|
|
2790
2879
|
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Aligns each list index's .index with its position in the new list.
|
|
2882
|
+
* A diff only becomes the rendered state once the updater applies it: an
|
|
2883
|
+
* earlier diff in the same batch (two replacements in one microtask) may have
|
|
2884
|
+
* moved shared indexes toward a list that never got applied, and a cache hit
|
|
2885
|
+
* skips recomputation entirely — so every createListDiff return re-aligns.
|
|
2886
|
+
*/
|
|
2887
|
+
function syncListIndexes(newIndexes) {
|
|
2888
|
+
for (let i = 0; i < newIndexes.length; i++) {
|
|
2889
|
+
if (newIndexes[i].index !== i) {
|
|
2890
|
+
newIndexes[i].index = i;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2791
2894
|
/**
|
|
2792
2895
|
* Creates or updates list indexes by comparing old and new lists.
|
|
2793
2896
|
* Optimizes by reusing existing list indexes when values match.
|
|
@@ -2798,6 +2901,11 @@ function isSameList(oldList, newList) {
|
|
|
2798
2901
|
* @returns Array of list indexes for the new list
|
|
2799
2902
|
*/
|
|
2800
2903
|
function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
2904
|
+
const diff = computeListDiff(parentListIndex, rawOldList, rawNewList);
|
|
2905
|
+
syncListIndexes(diff.newIndexes);
|
|
2906
|
+
return diff;
|
|
2907
|
+
}
|
|
2908
|
+
function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
2801
2909
|
// Normalize inputs to arrays (handles null/undefined)
|
|
2802
2910
|
const oldList = (Array.isArray(rawOldList) && rawOldList.length > 0) ? rawOldList : EMPTY_LIST;
|
|
2803
2911
|
const newList = (Array.isArray(rawNewList) && rawNewList.length > 0) ? rawNewList : EMPTY_LIST;
|
|
@@ -2846,6 +2954,10 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2846
2954
|
addIndexSet: EMPTY_SET$1,
|
|
2847
2955
|
};
|
|
2848
2956
|
}
|
|
2957
|
+
if (newIndexes !== null) {
|
|
2958
|
+
return calcDiffIndexes(oldIndexes, newIndexes);
|
|
2959
|
+
}
|
|
2960
|
+
newIndexes = [];
|
|
2849
2961
|
// Use index-based map for efficiency
|
|
2850
2962
|
// Supports duplicate values by storing array of indexes
|
|
2851
2963
|
const indexByValue = new Map();
|
|
@@ -2858,10 +2970,6 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2858
2970
|
}
|
|
2859
2971
|
indexes.push(i);
|
|
2860
2972
|
}
|
|
2861
|
-
if (newIndexes !== null) {
|
|
2862
|
-
return calcDiffIndexes(oldList, newList, oldIndexes, newIndexes, indexByValue);
|
|
2863
|
-
}
|
|
2864
|
-
newIndexes = [];
|
|
2865
2973
|
// Build new indexes array by matching values with old list
|
|
2866
2974
|
const changeIndexSet = new Set();
|
|
2867
2975
|
const addIndexSet = new Set();
|
|
@@ -2878,9 +2986,11 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2878
2986
|
else {
|
|
2879
2987
|
// Reuse existing element
|
|
2880
2988
|
const existingListIndex = oldIndexes[oldIndex];
|
|
2881
|
-
//
|
|
2882
|
-
|
|
2883
|
-
|
|
2989
|
+
// Judge position change against the old list's order (oldIndexes array
|
|
2990
|
+
// order), not the mutable .index — an earlier diff in the same batch
|
|
2991
|
+
// may have already moved .index toward a list that was never applied.
|
|
2992
|
+
// The .index itself is re-aligned by syncListIndexes on return.
|
|
2993
|
+
if (oldIndex !== i) {
|
|
2884
2994
|
changeIndexSet.add(existingListIndex);
|
|
2885
2995
|
}
|
|
2886
2996
|
newIndexes.push(existingListIndex);
|
|
@@ -2902,22 +3012,35 @@ function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
2902
3012
|
}
|
|
2903
3013
|
}
|
|
2904
3014
|
}
|
|
2905
|
-
|
|
3015
|
+
/**
|
|
3016
|
+
* Diff between two lists whose listIndex ledgers both already exist.
|
|
3017
|
+
* Rows are joined by listIndex identity — the same key applyChangeToFor and
|
|
3018
|
+
* walkDependency consume the result sets with. A value-based join could mark
|
|
3019
|
+
* oldIndexes-side objects that are absent from newIndexes (ledgers built along
|
|
3020
|
+
* unconnected diff chains hold different objects for the same value); such
|
|
3021
|
+
* orphan markers never match the consumers' has() lookups and only pollute
|
|
3022
|
+
* the dirty set. Rows without shared identity are represented as add+delete.
|
|
3023
|
+
*/
|
|
3024
|
+
function calcDiffIndexes(oldIndexes, newIndexes) {
|
|
2906
3025
|
const newIndexSet = new Set(newIndexes);
|
|
2907
3026
|
const oldIndexSet = new Set(oldIndexes);
|
|
2908
3027
|
const changeIndexSet = new Set();
|
|
2909
3028
|
const addIndexSet = newIndexSet.difference(oldIndexSet);
|
|
2910
3029
|
const deleteIndexSet = oldIndexSet.difference(newIndexSet);
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3030
|
+
// Old positions come from the oldIndexes array order (.index may have been
|
|
3031
|
+
// mutated by an unapplied diff in the same batch).
|
|
3032
|
+
const oldPosByIndex = new Map();
|
|
3033
|
+
for (let i = 0; i < oldIndexes.length; i++) {
|
|
3034
|
+
oldPosByIndex.set(oldIndexes[i], i);
|
|
3035
|
+
}
|
|
3036
|
+
for (let i = 0; i < newIndexes.length; i++) {
|
|
3037
|
+
const index = newIndexes[i];
|
|
3038
|
+
if (addIndexSet.has(index)) {
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
if (oldPosByIndex.get(index) !== i) {
|
|
3042
|
+
// 位置が違うことだけを記録
|
|
3043
|
+
changeIndexSet.add(index);
|
|
2921
3044
|
}
|
|
2922
3045
|
}
|
|
2923
3046
|
return {
|
|
@@ -3047,12 +3170,18 @@ function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
|
3047
3170
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
3048
3171
|
const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
|
|
3049
3172
|
bindingSet.add(binding);
|
|
3173
|
+
if (devtoolsSink !== null) {
|
|
3174
|
+
devtoolsSink({ type: "state:binding-added", absoluteAddress: absoluteStateAddress, binding });
|
|
3175
|
+
}
|
|
3050
3176
|
}
|
|
3051
3177
|
function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
3052
3178
|
// get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
|
|
3053
3179
|
const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3054
3180
|
if (bindingSet !== undefined) {
|
|
3055
3181
|
bindingSet.delete(binding);
|
|
3182
|
+
if (devtoolsSink !== null) {
|
|
3183
|
+
devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
|
|
3184
|
+
}
|
|
3056
3185
|
}
|
|
3057
3186
|
}
|
|
3058
3187
|
|
|
@@ -4744,840 +4873,1133 @@ async function buildBindings(root) {
|
|
|
4744
4873
|
}
|
|
4745
4874
|
}
|
|
4746
4875
|
|
|
4747
|
-
|
|
4748
|
-
|
|
4876
|
+
var version = "1.20.0";
|
|
4877
|
+
var pkg = {
|
|
4878
|
+
version: version};
|
|
4879
|
+
|
|
4880
|
+
const VERSION = pkg.version;
|
|
4881
|
+
|
|
4882
|
+
// SSR コメントパターン
|
|
4883
|
+
const SSR_PLACEHOLDER_COMMENT = /^@@wcs-(?:for|if|elseif|else):[^-]/;
|
|
4884
|
+
const SSR_BLOCK_START = /^@@wcs-(for|if|elseif|else)-start:(.+)$/;
|
|
4885
|
+
const SSR_BLOCK_END = /^@@wcs-(for|if|elseif|else)-end:(.+)$/;
|
|
4886
|
+
const SSR_TEXT_START = /^@@wcs-text-start:(.+)$/;
|
|
4749
4887
|
/**
|
|
4750
|
-
*
|
|
4888
|
+
* script 要素へ埋め込む JSON を HTML パーサから保護する。
|
|
4889
|
+
* HTML 直列化時、script の中身は生のまま出力されるため、state 値に
|
|
4890
|
+
* "</script>" や "<!--" を含む文字列があると script を脱出できてしまう。
|
|
4891
|
+
* "<" ">" "&" と U+2028/U+2029 を JSON の \uXXXX エスケープへ置換する
|
|
4892
|
+
* (JSON.parse では元の文字列と等価に復元される)。
|
|
4751
4893
|
*/
|
|
4752
|
-
function
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4894
|
+
function escapeJsonForScript(json) {
|
|
4895
|
+
return json
|
|
4896
|
+
.replace(/</g, '\\u003c')
|
|
4897
|
+
.replace(/>/g, '\\u003e')
|
|
4898
|
+
.replace(/&/g, '\\u0026')
|
|
4899
|
+
.replace(/\u2028/g, '\\u2028')
|
|
4900
|
+
.replace(/\u2029/g, '\\u2029');
|
|
4901
|
+
}
|
|
4902
|
+
class Ssr extends HTMLElement {
|
|
4903
|
+
_stateData = null;
|
|
4904
|
+
_templates = null;
|
|
4905
|
+
_hydrateProps = null;
|
|
4906
|
+
get name() {
|
|
4907
|
+
return this.getAttribute('name') || 'default';
|
|
4759
4908
|
}
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
const parts = info.split(':');
|
|
4767
|
-
let uuid;
|
|
4768
|
-
let path;
|
|
4769
|
-
let index = null;
|
|
4770
|
-
if (type === 'for') {
|
|
4771
|
-
// uuid:path:index
|
|
4772
|
-
uuid = parts[0];
|
|
4773
|
-
path = parts[1];
|
|
4774
|
-
index = parseInt(parts[2], 10);
|
|
4909
|
+
get version() {
|
|
4910
|
+
return this.getAttribute('version') || '';
|
|
4911
|
+
}
|
|
4912
|
+
get stateData() {
|
|
4913
|
+
if (this._stateData === null) {
|
|
4914
|
+
this._stateData = this._loadStateData();
|
|
4775
4915
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4916
|
+
return this._stateData;
|
|
4917
|
+
}
|
|
4918
|
+
get templates() {
|
|
4919
|
+
if (this._templates === null) {
|
|
4920
|
+
this._templates = this._loadTemplates();
|
|
4780
4921
|
}
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
4787
|
-
break;
|
|
4788
|
-
}
|
|
4789
|
-
nodes.push(sibling);
|
|
4790
|
-
sibling = sibling.nextSibling;
|
|
4922
|
+
return this._templates;
|
|
4923
|
+
}
|
|
4924
|
+
get hydrateProps() {
|
|
4925
|
+
if (this._hydrateProps === null) {
|
|
4926
|
+
this._hydrateProps = this._loadHydrateProps();
|
|
4791
4927
|
}
|
|
4792
|
-
|
|
4928
|
+
return this._hydrateProps;
|
|
4793
4929
|
}
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
/**
|
|
4797
|
-
* live DOM ノード群からバインディングを収集する。
|
|
4798
|
-
* ノードを一時的に DocumentFragment に移動して collectNodesAndBindingInfos を実行し、
|
|
4799
|
-
* 元の位置に戻す。
|
|
4800
|
-
*/
|
|
4801
|
-
function collectBindingsFromLiveNodes(nodes) {
|
|
4802
|
-
if (nodes.length === 0)
|
|
4803
|
-
return { bindingInfos: [], subscriberNodes: [] };
|
|
4804
|
-
// ノードの元の位置を記録
|
|
4805
|
-
const parent = nodes[0].parentNode;
|
|
4806
|
-
const nextSibling = nodes[nodes.length - 1].nextSibling;
|
|
4807
|
-
// 一時的に wrapper 要素に移動(collectNodesAndBindingInfos は Element を受け付ける)
|
|
4808
|
-
const wrapper = document.createElement('div');
|
|
4809
|
-
for (const node of nodes) {
|
|
4810
|
-
wrapper.appendChild(node);
|
|
4930
|
+
getTemplate(uuid) {
|
|
4931
|
+
return this.templates.get(uuid) ?? null;
|
|
4811
4932
|
}
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
if (
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4933
|
+
/**
|
|
4934
|
+
* サーバーの SSR バージョンとクライアントの state バージョンを検証する。
|
|
4935
|
+
* メジャー・マイナーバージョンが一致すればtrue。
|
|
4936
|
+
* version 属性がない場合は検証スキップ(true)。
|
|
4937
|
+
*/
|
|
4938
|
+
verifyVersion() {
|
|
4939
|
+
const serverVersion = this.version;
|
|
4940
|
+
if (!serverVersion)
|
|
4941
|
+
return true;
|
|
4942
|
+
const serverParts = serverVersion.split('.');
|
|
4943
|
+
const clientParts = VERSION.split('.');
|
|
4944
|
+
// メジャー・マイナーが一致すれば互換
|
|
4945
|
+
return serverParts[0] === clientParts[0] && serverParts[1] === clientParts[1];
|
|
4824
4946
|
}
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
while (wrapper.firstChild) {
|
|
4828
|
-
parent.insertBefore(wrapper.firstChild, nextSibling);
|
|
4829
|
-
}
|
|
4947
|
+
setStateData(data) {
|
|
4948
|
+
this._stateData = data;
|
|
4830
4949
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
// for ブロックの listIndex を UUID ごとに収集
|
|
4841
|
-
const listIndexesByUuid = new Map();
|
|
4842
|
-
for (const block of blocks) {
|
|
4843
|
-
if (block.nodes.length === 0)
|
|
4844
|
-
continue;
|
|
4845
|
-
const content = createContentFromNodes(block.nodes);
|
|
4846
|
-
// Content のバインディングを収集
|
|
4847
|
-
const { bindingInfos, subscriberNodes } = collectBindingsFromLiveNodes(block.nodes);
|
|
4848
|
-
// Content 内のノードに data-wcs-completed を付与
|
|
4849
|
-
// (メインの collectNodesAndBindingInfos で重複登録されないようにする)
|
|
4850
|
-
for (const node of subscriberNodes) {
|
|
4851
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4852
|
-
node.setAttribute('data-wcs-completed', '');
|
|
4853
|
-
}
|
|
4950
|
+
setHydrateProps(props) {
|
|
4951
|
+
this._hydrateProps = props;
|
|
4952
|
+
}
|
|
4953
|
+
_loadStateData() {
|
|
4954
|
+
const script = this.querySelector(`script[type="application/json"]:not([data-wcs-ssr-props])`);
|
|
4955
|
+
if (!script)
|
|
4956
|
+
return {};
|
|
4957
|
+
try {
|
|
4958
|
+
return JSON.parse(script.textContent || '{}');
|
|
4854
4959
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4960
|
+
catch {
|
|
4961
|
+
return {};
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
_loadTemplates() {
|
|
4965
|
+
const map = new Map();
|
|
4966
|
+
const templates = this.querySelectorAll('template[id]');
|
|
4967
|
+
for (const tpl of templates) {
|
|
4968
|
+
const id = tpl.getAttribute('id');
|
|
4969
|
+
if (id) {
|
|
4970
|
+
map.set(id, tpl);
|
|
4861
4971
|
}
|
|
4862
4972
|
}
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4973
|
+
return map;
|
|
4974
|
+
}
|
|
4975
|
+
_loadHydrateProps() {
|
|
4976
|
+
const script = this.querySelector('script[data-wcs-ssr-props]');
|
|
4977
|
+
if (!script)
|
|
4978
|
+
return {};
|
|
4979
|
+
try {
|
|
4980
|
+
return JSON.parse(script.textContent || '{}');
|
|
4981
|
+
}
|
|
4982
|
+
catch {
|
|
4983
|
+
return {};
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
static findByName(root, name) {
|
|
4987
|
+
const tagName = config.tagNames.ssr;
|
|
4988
|
+
const parentEl = root instanceof Element
|
|
4989
|
+
? root
|
|
4990
|
+
: root instanceof Document
|
|
4991
|
+
? root.documentElement
|
|
4992
|
+
: null;
|
|
4993
|
+
if (!parentEl)
|
|
4994
|
+
return null;
|
|
4995
|
+
const el = parentEl.querySelector(`${tagName}[name="${name}"]`);
|
|
4996
|
+
return el;
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* stateData と構造テンプレート・プロパティから <wcs-ssr> の中身を構築する。
|
|
5000
|
+
* server パッケージの renderToString から呼ばれる。
|
|
5001
|
+
*/
|
|
5002
|
+
/**
|
|
5003
|
+
* wcs-state 要素から $ プレフィックスや関数を除いたデータを抽出する。
|
|
5004
|
+
*/
|
|
5005
|
+
static extractStateData(stateEl) {
|
|
5006
|
+
const raw = stateEl.__state;
|
|
5007
|
+
if (!raw || typeof raw !== 'object')
|
|
5008
|
+
return {};
|
|
5009
|
+
const data = {};
|
|
5010
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
5011
|
+
if (!key.startsWith('$') && typeof value !== 'function') {
|
|
5012
|
+
data[key] = value;
|
|
4888
5013
|
}
|
|
4889
5014
|
}
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
5015
|
+
return data;
|
|
5016
|
+
}
|
|
5017
|
+
static buildContent(ssrEl, stateData) {
|
|
5018
|
+
// 初期データ JSON
|
|
5019
|
+
const jsonScript = document.createElement('script');
|
|
5020
|
+
jsonScript.setAttribute('type', 'application/json');
|
|
5021
|
+
jsonScript.textContent = escapeJsonForScript(JSON.stringify(stateData));
|
|
5022
|
+
ssrEl.appendChild(jsonScript);
|
|
5023
|
+
// UUID で管理されているテンプレートを復元して格納
|
|
5024
|
+
const uuids = getAllFragmentUUIDs();
|
|
5025
|
+
for (const uuid of uuids) {
|
|
5026
|
+
const fragmentInfo = getFragmentInfoByUUID(uuid);
|
|
5027
|
+
if (!fragmentInfo)
|
|
5028
|
+
continue;
|
|
5029
|
+
const tpl = document.createElement('template');
|
|
5030
|
+
tpl.setAttribute('id', uuid);
|
|
5031
|
+
const bindResult = fragmentInfo.parseBindTextResult;
|
|
5032
|
+
const bindText = bindResult.bindingType === 'else'
|
|
5033
|
+
? 'else:'
|
|
5034
|
+
: `${bindResult.bindingType}: ${bindResult.statePathName}`;
|
|
5035
|
+
tpl.setAttribute(config.bindAttributeName, bindText);
|
|
5036
|
+
const content = fragmentInfo.fragment.cloneNode(true);
|
|
5037
|
+
tpl.content.appendChild(content);
|
|
5038
|
+
ssrEl.appendChild(tpl);
|
|
5039
|
+
}
|
|
5040
|
+
// 属性で代替不可なプロパティをハイドレーション用に格納
|
|
5041
|
+
const ssrNodes = getAllSsrPropertyNodes();
|
|
5042
|
+
if (ssrNodes.length > 0) {
|
|
5043
|
+
const propsData = {};
|
|
5044
|
+
for (let i = 0; i < ssrNodes.length; i++) {
|
|
5045
|
+
const node = ssrNodes[i];
|
|
5046
|
+
const entries = getSsrProperties(node);
|
|
5047
|
+
if (entries.length === 0)
|
|
5048
|
+
continue;
|
|
5049
|
+
const id = `wcs-ssr-${i}`;
|
|
5050
|
+
node.setAttribute('data-wcs-ssr-id', id);
|
|
5051
|
+
const props = {};
|
|
5052
|
+
for (const entry of entries) {
|
|
5053
|
+
props[entry.propName] = entry.value;
|
|
4898
5054
|
}
|
|
5055
|
+
propsData[id] = props;
|
|
5056
|
+
}
|
|
5057
|
+
if (Object.keys(propsData).length > 0) {
|
|
5058
|
+
const propsScript = document.createElement('script');
|
|
5059
|
+
propsScript.setAttribute('type', 'application/json');
|
|
5060
|
+
propsScript.setAttribute('data-wcs-ssr-props', '');
|
|
5061
|
+
propsScript.textContent = escapeJsonForScript(JSON.stringify(propsData));
|
|
5062
|
+
ssrEl.appendChild(propsScript);
|
|
4899
5063
|
}
|
|
4900
5064
|
}
|
|
5065
|
+
clearSsrPropertyStore();
|
|
4901
5066
|
}
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
continue;
|
|
4913
|
-
const stateName = fragmentInfo.parseBindTextResult.stateName;
|
|
4914
|
-
const statePathName = fragmentInfo.parseBindTextResult.statePathName;
|
|
4915
|
-
const stateElement = getStateElementByName(rootNode, stateName);
|
|
4916
|
-
if (!stateElement)
|
|
4917
|
-
continue;
|
|
4918
|
-
stateElement.createState("readonly", (state) => {
|
|
4919
|
-
const list = state[statePathName];
|
|
4920
|
-
if (Array.isArray(list)) {
|
|
4921
|
-
setListIndexesByList(list, indexes);
|
|
5067
|
+
/**
|
|
5068
|
+
* SSR ブロック境界コメント (@@wcs-*-start/end) を除去する
|
|
5069
|
+
*/
|
|
5070
|
+
static removeBlockBoundaryComments(root) {
|
|
5071
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
5072
|
+
const toRemove = [];
|
|
5073
|
+
while (walker.nextNode()) {
|
|
5074
|
+
const comment = walker.currentNode;
|
|
5075
|
+
if (SSR_BLOCK_START.test(comment.data) || SSR_BLOCK_END.test(comment.data)) {
|
|
5076
|
+
toRemove.push(comment);
|
|
4922
5077
|
}
|
|
4923
|
-
}
|
|
5078
|
+
}
|
|
5079
|
+
for (const comment of toRemove) {
|
|
5080
|
+
comment.remove();
|
|
5081
|
+
}
|
|
4924
5082
|
}
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
const comment = walker.currentNode;
|
|
4940
|
-
if (comment.data === pattern) {
|
|
4941
|
-
return comment;
|
|
5083
|
+
/**
|
|
5084
|
+
* SSR の構造プレースホルダーコメント (@@wcs-for:uuid 等) を除去する
|
|
5085
|
+
*/
|
|
5086
|
+
static removeStructuralComments(root) {
|
|
5087
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
5088
|
+
const toRemove = [];
|
|
5089
|
+
while (walker.nextNode()) {
|
|
5090
|
+
const comment = walker.currentNode;
|
|
5091
|
+
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
5092
|
+
toRemove.push(comment);
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
for (const comment of toRemove) {
|
|
5096
|
+
comment.remove();
|
|
4942
5097
|
}
|
|
4943
5098
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
// elseif: 独自条件を持つが stateName は if から引き継ぐ
|
|
4959
|
-
if (bindingType === 'else' && lastIfParseResult) {
|
|
4960
|
-
parseBindTextResult = {
|
|
4961
|
-
...lastIfParseResult,
|
|
4962
|
-
outFilters: [...lastIfParseResult.outFilters, createNotFilter()],
|
|
4963
|
-
bindingType: 'else',
|
|
4964
|
-
};
|
|
5099
|
+
/**
|
|
5100
|
+
* SSR テキストバインディングコメントを復元する。
|
|
5101
|
+
* <!--@@wcs-text-start:path-->text<!--@@wcs-text-end:path-->
|
|
5102
|
+
* → <!--@@: path--> (バインディングシステムが認識する形式)
|
|
5103
|
+
*/
|
|
5104
|
+
static restoreTextBindings(root) {
|
|
5105
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
5106
|
+
const startComments = [];
|
|
5107
|
+
while (walker.nextNode()) {
|
|
5108
|
+
const comment = walker.currentNode;
|
|
5109
|
+
const match = SSR_TEXT_START.exec(comment.data);
|
|
5110
|
+
if (match) {
|
|
5111
|
+
startComments.push({ comment, path: match[1] });
|
|
5112
|
+
}
|
|
4965
5113
|
}
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
5114
|
+
for (const { comment, path } of startComments) {
|
|
5115
|
+
const bindComment = document.createComment(`@@: ${path}`);
|
|
5116
|
+
comment.parentNode.insertBefore(bindComment, comment);
|
|
5117
|
+
let sibling = comment.nextSibling;
|
|
5118
|
+
comment.remove();
|
|
5119
|
+
const endPattern = `@@wcs-text-end:${path}`;
|
|
5120
|
+
while (sibling) {
|
|
5121
|
+
const next = sibling.nextSibling;
|
|
5122
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5123
|
+
sibling.parentNode.removeChild(sibling);
|
|
5124
|
+
break;
|
|
5125
|
+
}
|
|
5126
|
+
sibling.parentNode.removeChild(sibling);
|
|
5127
|
+
sibling = next;
|
|
5128
|
+
}
|
|
4971
5129
|
}
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
5132
|
+
* SSR DOM をクリーンアップし、buildBindings が動作できる状態に戻す。
|
|
5133
|
+
* バージョン不一致時のフォールバック用。
|
|
5134
|
+
*
|
|
5135
|
+
* 1. SSR ブロック境界コメント間のレンダリング済みノードを除去
|
|
5136
|
+
* 2. SSR テキストバインディングを @@: 形式に復元
|
|
5137
|
+
* 3. プレースホルダーコメントを <wcs-ssr> 内のテンプレートで差し替え
|
|
5138
|
+
* 4. data-wcs-ssr-id 属性を除去
|
|
5139
|
+
* 5. <wcs-ssr> を除去
|
|
5140
|
+
*/
|
|
5141
|
+
static cleanupDom(root) {
|
|
5142
|
+
const body = document.body;
|
|
5143
|
+
// <wcs-ssr> からテンプレート UUID マップを構築(カスタム要素未定義でも動作するよう DOM 直接走査)
|
|
5144
|
+
const ssrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
5145
|
+
const templateByUuid = new Map();
|
|
5146
|
+
for (const ssrNode of ssrElements) {
|
|
5147
|
+
const templates = ssrNode.querySelectorAll('template[id]');
|
|
5148
|
+
for (const tpl of templates) {
|
|
5149
|
+
const id = tpl.getAttribute('id');
|
|
5150
|
+
if (id) {
|
|
5151
|
+
templateByUuid.set(id, tpl);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
4975
5154
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
5155
|
+
// SSR ブロック境界コメント間のレンダリング済みノードと境界コメントを除去
|
|
5156
|
+
const walker1 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
5157
|
+
const startComments = [];
|
|
5158
|
+
while (walker1.nextNode()) {
|
|
5159
|
+
const comment = walker1.currentNode;
|
|
5160
|
+
if (SSR_BLOCK_START.test(comment.data)) {
|
|
5161
|
+
startComments.push(comment);
|
|
5162
|
+
}
|
|
4978
5163
|
}
|
|
4979
|
-
|
|
4980
|
-
|
|
5164
|
+
for (const startComment of startComments) {
|
|
5165
|
+
const match = SSR_BLOCK_START.exec(startComment.data);
|
|
5166
|
+
const type = match[1];
|
|
5167
|
+
const info = match[2];
|
|
5168
|
+
const endPattern = `@@wcs-${type}-end:${info}`;
|
|
5169
|
+
let sibling = startComment.nextSibling;
|
|
5170
|
+
while (sibling) {
|
|
5171
|
+
const next = sibling.nextSibling;
|
|
5172
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5173
|
+
sibling.remove();
|
|
5174
|
+
break;
|
|
5175
|
+
}
|
|
5176
|
+
sibling.remove();
|
|
5177
|
+
sibling = next;
|
|
5178
|
+
}
|
|
5179
|
+
startComment.remove();
|
|
4981
5180
|
}
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5181
|
+
// SSR テキストバインディングを @@: 形式に復元
|
|
5182
|
+
Ssr.restoreTextBindings(body);
|
|
5183
|
+
// プレースホルダーコメント (@@wcs-for:uuid 等) をテンプレートに差し替え
|
|
5184
|
+
const walker2 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
5185
|
+
const placeholders = [];
|
|
5186
|
+
while (walker2.nextNode()) {
|
|
5187
|
+
const comment = walker2.currentNode;
|
|
5188
|
+
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
5189
|
+
const uuid = comment.data.split(':')[1];
|
|
5190
|
+
placeholders.push({ comment, uuid });
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
for (const { comment, uuid } of placeholders) {
|
|
5194
|
+
const tpl = templateByUuid.get(uuid);
|
|
5195
|
+
if (tpl) {
|
|
5196
|
+
const restored = document.createElement('template');
|
|
5197
|
+
const bindAttr = tpl.getAttribute(config.bindAttributeName);
|
|
5198
|
+
if (bindAttr)
|
|
5199
|
+
restored.setAttribute(config.bindAttributeName, bindAttr);
|
|
5200
|
+
const imported = document.importNode(tpl.content, true);
|
|
5201
|
+
if (imported.childNodes.length > 0) {
|
|
5202
|
+
restored.content.appendChild(imported);
|
|
5203
|
+
}
|
|
5204
|
+
else {
|
|
5205
|
+
for (const child of Array.from(tpl.childNodes)) {
|
|
5206
|
+
restored.content.appendChild(document.importNode(child, true));
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
comment.parentNode.replaceChild(restored, comment);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
// data-wcs-ssr-id 属性を除去
|
|
5213
|
+
const ssrIdElements = root.querySelectorAll('[data-wcs-ssr-id]');
|
|
5214
|
+
for (const el of ssrIdElements) {
|
|
5215
|
+
el.removeAttribute('data-wcs-ssr-id');
|
|
5216
|
+
}
|
|
5217
|
+
// <wcs-ssr> を除去
|
|
5218
|
+
for (const el of ssrElements) {
|
|
5219
|
+
el.remove();
|
|
4987
5220
|
}
|
|
4988
|
-
collectStructuralFragments(rootNode, fragment, forPath);
|
|
4989
|
-
const fragmentInfo = {
|
|
4990
|
-
fragment,
|
|
4991
|
-
parseBindTextResult,
|
|
4992
|
-
nodeInfos: getFragmentNodeInfos(fragment),
|
|
4993
|
-
};
|
|
4994
|
-
setFragmentInfoByUUID(uuid, rootNode, fragmentInfo);
|
|
4995
5221
|
}
|
|
4996
5222
|
}
|
|
5223
|
+
|
|
5224
|
+
// ハイドレーション時にスキップするバインディングタイプ
|
|
5225
|
+
const STRUCTURAL_TYPES = new Set(['for', 'if', 'elseif', 'else']);
|
|
4997
5226
|
/**
|
|
4998
|
-
* SSR
|
|
4999
|
-
* バージョン不一致時は DOM をクリーンアップして false を返す
|
|
5000
|
-
* (呼び出し元で buildBindings にフォールバック)。
|
|
5227
|
+
* SSR ブロック境界コメントを走査して、start〜end 間のノードを収集する
|
|
5001
5228
|
*/
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
const
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
console.warn(`[@wcstack/state] SSR version mismatch: server="${ssrEl.version}", client="${VERSION}". Falling back to full render.`);
|
|
5010
|
-
Ssr.cleanupDom(root);
|
|
5011
|
-
return false;
|
|
5012
|
-
}
|
|
5013
|
-
}
|
|
5014
|
-
// <wcs-ssr> からテンプレートを fragmentInfoByUUID に復帰
|
|
5015
|
-
for (const ssrNode of ssrElements) {
|
|
5016
|
-
restoreFragments(root, ssrNode);
|
|
5017
|
-
}
|
|
5018
|
-
// SSR ブロック境界コメントから既存 DOM を Content 化
|
|
5019
|
-
const blocks = collectSsrBlocks(document.body);
|
|
5020
|
-
hydrateBlocks(document.body, blocks);
|
|
5021
|
-
// ブロック境界コメント (start/end) を除去
|
|
5022
|
-
Ssr.removeBlockBoundaryComments(document.body);
|
|
5023
|
-
// <wcs-ssr> を一時除去(バインディング走査に含めない)
|
|
5024
|
-
const ssrParents = [];
|
|
5025
|
-
for (const el of ssrElements) {
|
|
5026
|
-
if (el.parentNode) {
|
|
5027
|
-
ssrParents.push({ el, parent: el.parentNode, next: el.nextSibling });
|
|
5028
|
-
el.remove();
|
|
5029
|
-
}
|
|
5229
|
+
function collectSsrBlocks(root) {
|
|
5230
|
+
const blocks = [];
|
|
5231
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
5232
|
+
const startComments = [];
|
|
5233
|
+
// まず全コメントを収集
|
|
5234
|
+
while (walker.nextNode()) {
|
|
5235
|
+
startComments.push(walker.currentNode);
|
|
5030
5236
|
}
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5237
|
+
for (const comment of startComments) {
|
|
5238
|
+
const startMatch = SSR_BLOCK_START.exec(comment.data);
|
|
5239
|
+
if (!startMatch)
|
|
5240
|
+
continue;
|
|
5241
|
+
const type = startMatch[1];
|
|
5242
|
+
const info = startMatch[2]; // "uuid:path:index" or "uuid:path"
|
|
5243
|
+
const parts = info.split(':');
|
|
5244
|
+
let uuid;
|
|
5245
|
+
let path;
|
|
5246
|
+
let index = null;
|
|
5247
|
+
if (type === 'for') {
|
|
5248
|
+
// uuid:path:index
|
|
5249
|
+
uuid = parts[0];
|
|
5250
|
+
path = parts[1];
|
|
5251
|
+
index = parseInt(parts[2], 10);
|
|
5046
5252
|
}
|
|
5047
5253
|
else {
|
|
5048
|
-
//
|
|
5049
|
-
|
|
5254
|
+
// uuid:path
|
|
5255
|
+
uuid = parts[0];
|
|
5256
|
+
path = parts.slice(1).join(':');
|
|
5257
|
+
}
|
|
5258
|
+
// start と end の間のノードを収集
|
|
5259
|
+
const nodes = [];
|
|
5260
|
+
let sibling = comment.nextSibling;
|
|
5261
|
+
const endPattern = `@@wcs-${type}-end:${info}`;
|
|
5262
|
+
while (sibling) {
|
|
5263
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5264
|
+
break;
|
|
5265
|
+
}
|
|
5266
|
+
nodes.push(sibling);
|
|
5267
|
+
sibling = sibling.nextSibling;
|
|
5050
5268
|
}
|
|
5269
|
+
blocks.push({ type, uuid, path, index, nodes });
|
|
5270
|
+
}
|
|
5271
|
+
return blocks;
|
|
5272
|
+
}
|
|
5273
|
+
/**
|
|
5274
|
+
* live DOM ノード群からバインディングを収集する。
|
|
5275
|
+
* ノードを一時的に DocumentFragment に移動して collectNodesAndBindingInfos を実行し、
|
|
5276
|
+
* 元の位置に戻す。
|
|
5277
|
+
*/
|
|
5278
|
+
function collectBindingsFromLiveNodes(nodes) {
|
|
5279
|
+
if (nodes.length === 0)
|
|
5280
|
+
return { bindingInfos: [], subscriberNodes: [] };
|
|
5281
|
+
// ノードの元の位置を記録
|
|
5282
|
+
const parent = nodes[0].parentNode;
|
|
5283
|
+
const nextSibling = nodes[nodes.length - 1].nextSibling;
|
|
5284
|
+
// 一時的に wrapper 要素に移動(collectNodesAndBindingInfos は Element を受け付ける)
|
|
5285
|
+
const wrapper = document.createElement('div');
|
|
5286
|
+
for (const node of nodes) {
|
|
5287
|
+
wrapper.appendChild(node);
|
|
5051
5288
|
}
|
|
5052
|
-
//
|
|
5053
|
-
const
|
|
5054
|
-
|
|
5289
|
+
// バインディング収集
|
|
5290
|
+
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(wrapper);
|
|
5291
|
+
// _initializeBindings 相当の処理
|
|
5055
5292
|
for (const binding of allBindings) {
|
|
5056
5293
|
replaceToReplaceNode(binding);
|
|
5057
|
-
if (attachEventHandler(binding))
|
|
5294
|
+
if (attachEventHandler(binding))
|
|
5058
5295
|
continue;
|
|
5059
|
-
|
|
5060
|
-
if (attachEventTokenHandler(binding)) {
|
|
5296
|
+
if (attachEventTokenHandler(binding))
|
|
5061
5297
|
continue;
|
|
5062
|
-
}
|
|
5063
5298
|
attachTwowayEventHandler(binding);
|
|
5064
5299
|
attachRadioEventHandler(binding);
|
|
5065
5300
|
attachCheckboxEventHandler(binding);
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
continue;
|
|
5072
|
-
}
|
|
5073
|
-
else {
|
|
5074
|
-
normalBindings.push(binding);
|
|
5301
|
+
}
|
|
5302
|
+
// 元の位置に戻す
|
|
5303
|
+
if (parent) {
|
|
5304
|
+
while (wrapper.firstChild) {
|
|
5305
|
+
parent.insertBefore(wrapper.firstChild, nextSibling);
|
|
5075
5306
|
}
|
|
5076
5307
|
}
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5308
|
+
return {
|
|
5309
|
+
bindingInfos: allBindings,
|
|
5310
|
+
subscriberNodes,
|
|
5311
|
+
};
|
|
5312
|
+
}
|
|
5313
|
+
/**
|
|
5314
|
+
* SSR ブロックの DOM ノードを Content 化し、バインディングを登録する。
|
|
5315
|
+
*/
|
|
5316
|
+
function hydrateBlocks(root, blocks) {
|
|
5317
|
+
// for ブロックの listIndex を UUID ごとに収集
|
|
5318
|
+
const listIndexesByUuid = new Map();
|
|
5319
|
+
for (const block of blocks) {
|
|
5320
|
+
if (block.nodes.length === 0)
|
|
5321
|
+
continue;
|
|
5322
|
+
const content = createContentFromNodes(block.nodes);
|
|
5323
|
+
// Content のバインディングを収集
|
|
5324
|
+
const { bindingInfos, subscriberNodes } = collectBindingsFromLiveNodes(block.nodes);
|
|
5325
|
+
// Content 内のノードに data-wcs-completed を付与
|
|
5326
|
+
// (メインの collectNodesAndBindingInfos で重複登録されないようにする)
|
|
5327
|
+
for (const node of subscriberNodes) {
|
|
5328
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
5329
|
+
node.setAttribute('data-wcs-completed', '');
|
|
5330
|
+
}
|
|
5085
5331
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5332
|
+
setBindingsByContent(content, bindingInfos);
|
|
5333
|
+
setNodesByContent(content, subscriberNodes);
|
|
5334
|
+
const indexBindings = [];
|
|
5335
|
+
for (const binding of bindingInfos) {
|
|
5336
|
+
if (binding.statePathName in INDEX_BY_INDEX_NAME) {
|
|
5337
|
+
indexBindings.push(binding);
|
|
5338
|
+
}
|
|
5088
5339
|
}
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5340
|
+
setIndexBindingsByContent(content, indexBindings);
|
|
5341
|
+
if (block.type === 'for' && block.index !== null) {
|
|
5342
|
+
const placeholderComment = findPlaceholderComment(root, 'for', block.uuid);
|
|
5343
|
+
if (placeholderComment) {
|
|
5344
|
+
const listIndex = createListIndex(null, block.index);
|
|
5345
|
+
hydrateSetContent(placeholderComment, listIndex, content);
|
|
5346
|
+
const lastNode = block.nodes[block.nodes.length - 1];
|
|
5347
|
+
hydrateSetLastNode(placeholderComment, lastNode);
|
|
5348
|
+
setContentByNode(placeholderComment, content);
|
|
5349
|
+
// ループコンテキストをバインドし、バインディングをアドレスに登録
|
|
5350
|
+
const pathInfo = getPathInfo(block.path + '.' + WILDCARD);
|
|
5351
|
+
const stateAddress = createStateAddress(pathInfo, listIndex);
|
|
5352
|
+
// ILoopContext は IStateAddress + listIndex なので、stateAddress をそのまま使う
|
|
5353
|
+
bindLoopContextToContent(content, stateAddress);
|
|
5354
|
+
for (const binding of bindingInfos) {
|
|
5355
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
5356
|
+
addBindingByAbsoluteStateAddress(absAddr, binding);
|
|
5357
|
+
}
|
|
5358
|
+
// listIndex を UUID ごとに収集(後で setListIndexesByList に渡す)
|
|
5359
|
+
let indexes = listIndexesByUuid.get(block.uuid);
|
|
5360
|
+
if (!indexes) {
|
|
5361
|
+
indexes = [];
|
|
5362
|
+
listIndexesByUuid.set(block.uuid, indexes);
|
|
5363
|
+
}
|
|
5364
|
+
indexes.push(listIndex);
|
|
5104
5365
|
}
|
|
5105
5366
|
}
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
for (const ssrNode of restoredSsrElements) {
|
|
5116
|
-
const ssrEl = ssrNode;
|
|
5117
|
-
const props = ssrEl.hydrateProps;
|
|
5118
|
-
for (const [id, propMap] of Object.entries(props)) {
|
|
5119
|
-
const target = root.querySelector(`[data-wcs-ssr-id="${id}"]`);
|
|
5120
|
-
if (!target)
|
|
5121
|
-
continue;
|
|
5122
|
-
for (const [propName, value] of Object.entries(propMap)) {
|
|
5123
|
-
target[propName] = value;
|
|
5367
|
+
else {
|
|
5368
|
+
const placeholderComment = findPlaceholderComment(root, block.type, block.uuid);
|
|
5369
|
+
if (placeholderComment) {
|
|
5370
|
+
setContentByNode(placeholderComment, content);
|
|
5371
|
+
// バインディングをアドレスに登録
|
|
5372
|
+
for (const binding of bindingInfos) {
|
|
5373
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
5374
|
+
addBindingByAbsoluteStateAddress(absAddr, binding);
|
|
5375
|
+
}
|
|
5124
5376
|
}
|
|
5125
5377
|
}
|
|
5126
5378
|
}
|
|
5127
|
-
//
|
|
5128
|
-
const
|
|
5129
|
-
|
|
5130
|
-
|
|
5379
|
+
// for ブロックの listIndex を state のリスト値に紐づける
|
|
5380
|
+
for (const [uuid, indexes] of listIndexesByUuid) {
|
|
5381
|
+
const placeholderComment = findPlaceholderComment(root, 'for', uuid);
|
|
5382
|
+
if (!placeholderComment)
|
|
5383
|
+
continue;
|
|
5384
|
+
// state から現在のリスト値を取得して listIndexes を設定
|
|
5385
|
+
const rootNode = placeholderComment.getRootNode();
|
|
5386
|
+
// structuralBindings はまだ登録前なので、getParseBindTextResults を直接使う
|
|
5387
|
+
const fragmentInfo = getFragmentInfoByUUID(uuid);
|
|
5388
|
+
if (!fragmentInfo)
|
|
5389
|
+
continue;
|
|
5390
|
+
const stateName = fragmentInfo.parseBindTextResult.stateName;
|
|
5391
|
+
const statePathName = fragmentInfo.parseBindTextResult.statePathName;
|
|
5392
|
+
const stateElement = getStateElementByName(rootNode, stateName);
|
|
5393
|
+
if (!stateElement)
|
|
5394
|
+
continue;
|
|
5395
|
+
stateElement.createState("readonly", (state) => {
|
|
5396
|
+
const list = state[statePathName];
|
|
5397
|
+
if (Array.isArray(list)) {
|
|
5398
|
+
setListIndexesByList(list, indexes);
|
|
5399
|
+
}
|
|
5400
|
+
});
|
|
5131
5401
|
}
|
|
5132
|
-
return true;
|
|
5133
5402
|
}
|
|
5134
|
-
|
|
5135
|
-
const
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5403
|
+
function findPlaceholderComment(root, type, uuid) {
|
|
5404
|
+
const keywordMap = {
|
|
5405
|
+
'for': config.commentForPrefix,
|
|
5406
|
+
'if': config.commentIfPrefix,
|
|
5407
|
+
'elseif': config.commentElseIfPrefix,
|
|
5408
|
+
'else': config.commentElsePrefix,
|
|
5409
|
+
};
|
|
5410
|
+
const keyword = keywordMap[type];
|
|
5411
|
+
if (!keyword)
|
|
5140
5412
|
return null;
|
|
5141
|
-
}
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
function getBindingsReady(rootNode) {
|
|
5148
|
-
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
5149
|
-
}
|
|
5150
|
-
function setStateElementByName(rootNode, name, element) {
|
|
5151
|
-
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
5152
|
-
if (element === null) {
|
|
5153
|
-
// 削除の場合、Mapが存在しない場合は何もしない
|
|
5154
|
-
if (!stateElementByName) {
|
|
5155
|
-
return;
|
|
5156
|
-
}
|
|
5157
|
-
stateElementByName.delete(name);
|
|
5158
|
-
if (stateElementByName.size === 0) {
|
|
5159
|
-
stateElementByNameByNode.delete(rootNode);
|
|
5160
|
-
}
|
|
5161
|
-
if (config.debug) {
|
|
5162
|
-
console.debug(`State element unregistered: name="${name}"`);
|
|
5163
|
-
}
|
|
5164
|
-
}
|
|
5165
|
-
else {
|
|
5166
|
-
// 登録の場合
|
|
5167
|
-
if (!stateElementByName) {
|
|
5168
|
-
stateElementByName = new Map();
|
|
5169
|
-
stateElementByNameByNode.set(rootNode, stateElementByName);
|
|
5170
|
-
// 初めてルートノードに登録する場合
|
|
5171
|
-
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
5172
|
-
const enableSsr = !inSsr() && element.hasAttribute?.('enable-ssr');
|
|
5173
|
-
if (rootNode.constructor.name === 'HTMLDocument' || rootNode.constructor.name === 'Document') {
|
|
5174
|
-
const ready = new Promise((resolve) => {
|
|
5175
|
-
queueMicrotask(async () => {
|
|
5176
|
-
if (enableSsr) {
|
|
5177
|
-
const success = await hydrateBindings(rootNode);
|
|
5178
|
-
if (!success) {
|
|
5179
|
-
await buildBindings(rootNode);
|
|
5180
|
-
}
|
|
5181
|
-
}
|
|
5182
|
-
else {
|
|
5183
|
-
await buildBindings(rootNode);
|
|
5184
|
-
}
|
|
5185
|
-
resolve();
|
|
5186
|
-
});
|
|
5187
|
-
});
|
|
5188
|
-
bindingsReadyByNode.set(rootNode, ready);
|
|
5189
|
-
}
|
|
5190
|
-
else if (rootNode.constructor.name === 'ShadowRoot') {
|
|
5191
|
-
const ready = new Promise((resolve) => {
|
|
5192
|
-
queueMicrotask(async () => {
|
|
5193
|
-
await buildBindings(rootNode);
|
|
5194
|
-
resolve();
|
|
5195
|
-
});
|
|
5196
|
-
});
|
|
5197
|
-
bindingsReadyByNode.set(rootNode, ready);
|
|
5198
|
-
}
|
|
5199
|
-
}
|
|
5200
|
-
if (stateElementByName.has(name)) {
|
|
5201
|
-
raiseError(`State element with name "${name}" is already registered.`);
|
|
5202
|
-
}
|
|
5203
|
-
stateElementByName.set(name, element);
|
|
5204
|
-
if (config.debug) {
|
|
5205
|
-
console.debug(`State element registered: name="${name}"`, element);
|
|
5413
|
+
const pattern = `@@${keyword}:${uuid}`;
|
|
5414
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
5415
|
+
while (walker.nextNode()) {
|
|
5416
|
+
const comment = walker.currentNode;
|
|
5417
|
+
if (comment.data === pattern) {
|
|
5418
|
+
return comment;
|
|
5206
5419
|
}
|
|
5207
5420
|
}
|
|
5421
|
+
return null;
|
|
5208
5422
|
}
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5423
|
+
/**
|
|
5424
|
+
* <wcs-ssr> 内のテンプレートを fragmentInfoByUUID に復帰させる。
|
|
5425
|
+
*/
|
|
5426
|
+
function restoreFragments(root, ssrEl) {
|
|
5427
|
+
const rootNode = root;
|
|
5428
|
+
let lastIfParseResult = null;
|
|
5429
|
+
for (const [uuid, tpl] of ssrEl.templates) {
|
|
5430
|
+
const bindText = tpl.getAttribute(config.bindAttributeName) || '';
|
|
5431
|
+
const parseBindTextResults = parseBindTextsForElement(bindText);
|
|
5432
|
+
let parseBindTextResult = parseBindTextResults[0];
|
|
5433
|
+
const bindingType = parseBindTextResult.bindingType;
|
|
5434
|
+
// else: 直前の if 条件の not → 条件反転
|
|
5435
|
+
// elseif: 独自条件を持つが stateName は if から引き継ぐ
|
|
5436
|
+
if (bindingType === 'else' && lastIfParseResult) {
|
|
5437
|
+
parseBindTextResult = {
|
|
5438
|
+
...lastIfParseResult,
|
|
5439
|
+
outFilters: [...lastIfParseResult.outFilters, createNotFilter()],
|
|
5440
|
+
bindingType: 'else',
|
|
5441
|
+
};
|
|
5221
5442
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5443
|
+
else if (bindingType === 'elseif' && lastIfParseResult) {
|
|
5444
|
+
parseBindTextResult = {
|
|
5445
|
+
...parseBindTextResult,
|
|
5446
|
+
stateName: lastIfParseResult.stateName,
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
// if chain の追跡
|
|
5450
|
+
if (bindingType === 'if') {
|
|
5451
|
+
lastIfParseResult = parseBindTextResult;
|
|
5452
|
+
}
|
|
5453
|
+
else if (bindingType === 'elseif') {
|
|
5454
|
+
lastIfParseResult = parseBindTextResult;
|
|
5455
|
+
}
|
|
5456
|
+
else if (bindingType === 'else') {
|
|
5457
|
+
lastIfParseResult = null;
|
|
5458
|
+
}
|
|
5459
|
+
const fragment = document.importNode(tpl.content, true);
|
|
5460
|
+
const forPath = bindingType === "for" ? parseBindTextResult.statePathName : undefined;
|
|
5461
|
+
optimizeFragment(fragment);
|
|
5462
|
+
if (typeof forPath === "string") {
|
|
5463
|
+
expandShorthandPaths(fragment, forPath);
|
|
5231
5464
|
}
|
|
5465
|
+
collectStructuralFragments(rootNode, fragment, forPath);
|
|
5466
|
+
const fragmentInfo = {
|
|
5467
|
+
fragment,
|
|
5468
|
+
parseBindTextResult,
|
|
5469
|
+
nodeInfos: getFragmentNodeInfos(fragment),
|
|
5470
|
+
};
|
|
5471
|
+
setFragmentInfoByUUID(uuid, rootNode, fragmentInfo);
|
|
5232
5472
|
}
|
|
5233
5473
|
}
|
|
5234
|
-
function getFragmentInfoByUUID(uuid) {
|
|
5235
|
-
return fragmentInfoByUUID.get(uuid) || null;
|
|
5236
|
-
}
|
|
5237
|
-
function getAllFragmentUUIDs() {
|
|
5238
|
-
return Array.from(fragmentInfoByUUID.keys());
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
|
-
// SSR コメントパターン
|
|
5242
|
-
const SSR_PLACEHOLDER_COMMENT = /^@@wcs-(?:for|if|elseif|else):[^-]/;
|
|
5243
|
-
const SSR_BLOCK_START = /^@@wcs-(for|if|elseif|else)-start:(.+)$/;
|
|
5244
|
-
const SSR_BLOCK_END = /^@@wcs-(for|if|elseif|else)-end:(.+)$/;
|
|
5245
|
-
const SSR_TEXT_START = /^@@wcs-text-start:(.+)$/;
|
|
5246
5474
|
/**
|
|
5247
|
-
*
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5250
|
-
* "<" ">" "&" と U+2028/U+2029 を JSON の \uXXXX エスケープへ置換する
|
|
5251
|
-
* (JSON.parse では元の文字列と等価に復元される)。
|
|
5475
|
+
* SSR ハイドレーション用バインディング初期化。
|
|
5476
|
+
* バージョン不一致時は DOM をクリーンアップして false を返す
|
|
5477
|
+
* (呼び出し元で buildBindings にフォールバック)。
|
|
5252
5478
|
*/
|
|
5253
|
-
function
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
.
|
|
5260
|
-
}
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
_hydrateProps = null;
|
|
5265
|
-
get name() {
|
|
5266
|
-
return this.getAttribute('name') || 'default';
|
|
5479
|
+
async function hydrateBindings(root) {
|
|
5480
|
+
await waitForStateInitialize(root);
|
|
5481
|
+
// バージョン検証
|
|
5482
|
+
const ssrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
5483
|
+
for (const ssrNode of ssrElements) {
|
|
5484
|
+
const ssrEl = ssrNode;
|
|
5485
|
+
if (!ssrEl.verifyVersion()) {
|
|
5486
|
+
console.warn(`[@wcstack/state] SSR version mismatch: server="${ssrEl.version}", client="${VERSION}". Falling back to full render.`);
|
|
5487
|
+
Ssr.cleanupDom(root);
|
|
5488
|
+
return false;
|
|
5489
|
+
}
|
|
5267
5490
|
}
|
|
5268
|
-
|
|
5269
|
-
|
|
5491
|
+
// <wcs-ssr> からテンプレートを fragmentInfoByUUID に復帰
|
|
5492
|
+
for (const ssrNode of ssrElements) {
|
|
5493
|
+
restoreFragments(root, ssrNode);
|
|
5270
5494
|
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5495
|
+
// SSR ブロック境界コメントから既存 DOM を Content 化
|
|
5496
|
+
const blocks = collectSsrBlocks(document.body);
|
|
5497
|
+
hydrateBlocks(document.body, blocks);
|
|
5498
|
+
// ブロック境界コメント (start/end) を除去
|
|
5499
|
+
Ssr.removeBlockBoundaryComments(document.body);
|
|
5500
|
+
// <wcs-ssr> を一時除去(バインディング走査に含めない)
|
|
5501
|
+
const ssrParents = [];
|
|
5502
|
+
for (const el of ssrElements) {
|
|
5503
|
+
if (el.parentNode) {
|
|
5504
|
+
ssrParents.push({ el, parent: el.parentNode, next: el.nextSibling });
|
|
5505
|
+
el.remove();
|
|
5274
5506
|
}
|
|
5275
|
-
return this._stateData;
|
|
5276
5507
|
}
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5508
|
+
// 構造プレースホルダーコメント (@@wcs-for:uuid 等) は残す
|
|
5509
|
+
// → バインディング走査で拾われ、状態変化時の再レンダリングに使われる
|
|
5510
|
+
// SSR テキストバインディングを @@: 形式に復元
|
|
5511
|
+
Ssr.restoreTextBindings(document.body);
|
|
5512
|
+
// ノードとバインディングを収集
|
|
5513
|
+
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(document.body);
|
|
5514
|
+
// 収集完了したノードに data-wcs-completed 属性を付与
|
|
5515
|
+
// for ブロック内ノード(hydrateBlocks で登録済み)にはループコンテキストをリセットしない
|
|
5516
|
+
for (const node of subscriberNodes) {
|
|
5517
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
5518
|
+
const el = node;
|
|
5519
|
+
if (!el.hasAttribute('data-wcs-completed')) {
|
|
5520
|
+
setLoopContextByNode(node, null);
|
|
5521
|
+
el.setAttribute('data-wcs-completed', '');
|
|
5522
|
+
}
|
|
5280
5523
|
}
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
if (this._hydrateProps === null) {
|
|
5285
|
-
this._hydrateProps = this._loadHydrateProps();
|
|
5524
|
+
else {
|
|
5525
|
+
// コメントノード等
|
|
5526
|
+
setLoopContextByNode(node, null);
|
|
5286
5527
|
}
|
|
5287
|
-
return this._hydrateProps;
|
|
5288
|
-
}
|
|
5289
|
-
getTemplate(uuid) {
|
|
5290
|
-
return this.templates.get(uuid) ?? null;
|
|
5291
|
-
}
|
|
5292
|
-
/**
|
|
5293
|
-
* サーバーの SSR バージョンとクライアントの state バージョンを検証する。
|
|
5294
|
-
* メジャー・マイナーバージョンが一致すればtrue。
|
|
5295
|
-
* version 属性がない場合は検証スキップ(true)。
|
|
5296
|
-
*/
|
|
5297
|
-
verifyVersion() {
|
|
5298
|
-
const serverVersion = this.version;
|
|
5299
|
-
if (!serverVersion)
|
|
5300
|
-
return true;
|
|
5301
|
-
const serverParts = serverVersion.split('.');
|
|
5302
|
-
const clientParts = VERSION.split('.');
|
|
5303
|
-
// メジャー・マイナーが一致すれば互換
|
|
5304
|
-
return serverParts[0] === clientParts[0] && serverParts[1] === clientParts[1];
|
|
5305
|
-
}
|
|
5306
|
-
setStateData(data) {
|
|
5307
|
-
this._stateData = data;
|
|
5308
|
-
}
|
|
5309
|
-
setHydrateProps(props) {
|
|
5310
|
-
this._hydrateProps = props;
|
|
5311
5528
|
}
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5529
|
+
// バインディングを構造系とそれ以外に分離
|
|
5530
|
+
const normalBindings = [];
|
|
5531
|
+
const structuralBindings = [];
|
|
5532
|
+
for (const binding of allBindings) {
|
|
5533
|
+
replaceToReplaceNode(binding);
|
|
5534
|
+
if (attachEventHandler(binding)) {
|
|
5535
|
+
continue;
|
|
5318
5536
|
}
|
|
5319
|
-
|
|
5320
|
-
|
|
5537
|
+
if (attachEventTokenHandler(binding)) {
|
|
5538
|
+
continue;
|
|
5321
5539
|
}
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
const id = tpl.getAttribute('id');
|
|
5328
|
-
if (id) {
|
|
5329
|
-
map.set(id, tpl);
|
|
5330
|
-
}
|
|
5540
|
+
attachTwowayEventHandler(binding);
|
|
5541
|
+
attachRadioEventHandler(binding);
|
|
5542
|
+
attachCheckboxEventHandler(binding);
|
|
5543
|
+
if (STRUCTURAL_TYPES.has(binding.bindingType)) {
|
|
5544
|
+
structuralBindings.push(binding);
|
|
5331
5545
|
}
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
const script = this.querySelector('script[data-wcs-ssr-props]');
|
|
5336
|
-
if (!script)
|
|
5337
|
-
return {};
|
|
5338
|
-
try {
|
|
5339
|
-
return JSON.parse(script.textContent || '{}');
|
|
5546
|
+
else if (binding.statePathName.includes(WILDCARD)) {
|
|
5547
|
+
// for ブロック内のバインディング → Content のバインディングとして登録済み
|
|
5548
|
+
continue;
|
|
5340
5549
|
}
|
|
5341
|
-
|
|
5342
|
-
|
|
5550
|
+
else {
|
|
5551
|
+
normalBindings.push(binding);
|
|
5343
5552
|
}
|
|
5344
5553
|
}
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
const
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5554
|
+
// 全バインディング(通常 + 構造)をアドレスに登録
|
|
5555
|
+
for (const binding of [...normalBindings, ...structuralBindings]) {
|
|
5556
|
+
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
5557
|
+
addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
5558
|
+
const rootNode = binding.replaceNode.getRootNode();
|
|
5559
|
+
const stateElement = getStateElementByName(rootNode, binding.stateName);
|
|
5560
|
+
if (stateElement === null) {
|
|
5561
|
+
raiseError(`State element with name "${binding.stateName}" not found for binding.`);
|
|
5562
|
+
}
|
|
5563
|
+
if (binding.bindingType !== 'event') {
|
|
5564
|
+
stateElement.setPathInfo(binding.statePathName, binding.bindingType);
|
|
5565
|
+
}
|
|
5356
5566
|
}
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
data[key] = value;
|
|
5567
|
+
// for バインディングの lastListValue を初期値として設定
|
|
5568
|
+
// (次回の状態変化時に差分計算の基準になる)
|
|
5569
|
+
for (const binding of structuralBindings) {
|
|
5570
|
+
if (binding.bindingType === 'for') {
|
|
5571
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
5572
|
+
const rootNode = binding.replaceNode.getRootNode();
|
|
5573
|
+
const stateElement = getStateElementByName(rootNode, binding.stateName);
|
|
5574
|
+
if (stateElement) {
|
|
5575
|
+
stateElement.createState("readonly", (state) => {
|
|
5576
|
+
const value = state[binding.statePathName];
|
|
5577
|
+
if (Array.isArray(value)) {
|
|
5578
|
+
setLastListValueByAbsoluteStateAddress(absAddr, value);
|
|
5579
|
+
}
|
|
5580
|
+
});
|
|
5372
5581
|
}
|
|
5373
5582
|
}
|
|
5374
|
-
return data;
|
|
5375
5583
|
}
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5584
|
+
// 通常バインディングのみ初回値適用(構造バインディングはSSR描画済み)
|
|
5585
|
+
applyChangeFromBindings(normalBindings);
|
|
5586
|
+
// <wcs-ssr> を元に戻す
|
|
5587
|
+
for (const { el, parent, next } of ssrParents) {
|
|
5588
|
+
parent.insertBefore(el, next);
|
|
5589
|
+
}
|
|
5590
|
+
// hydrateProps 復元
|
|
5591
|
+
const restoredSsrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
5592
|
+
for (const ssrNode of restoredSsrElements) {
|
|
5593
|
+
const ssrEl = ssrNode;
|
|
5594
|
+
const props = ssrEl.hydrateProps;
|
|
5595
|
+
for (const [id, propMap] of Object.entries(props)) {
|
|
5596
|
+
const target = root.querySelector(`[data-wcs-ssr-id="${id}"]`);
|
|
5597
|
+
if (!target)
|
|
5387
5598
|
continue;
|
|
5388
|
-
const
|
|
5389
|
-
|
|
5390
|
-
const bindResult = fragmentInfo.parseBindTextResult;
|
|
5391
|
-
const bindText = bindResult.bindingType === 'else'
|
|
5392
|
-
? 'else:'
|
|
5393
|
-
: `${bindResult.bindingType}: ${bindResult.statePathName}`;
|
|
5394
|
-
tpl.setAttribute(config.bindAttributeName, bindText);
|
|
5395
|
-
const content = fragmentInfo.fragment.cloneNode(true);
|
|
5396
|
-
tpl.content.appendChild(content);
|
|
5397
|
-
ssrEl.appendChild(tpl);
|
|
5398
|
-
}
|
|
5399
|
-
// 属性で代替不可なプロパティをハイドレーション用に格納
|
|
5400
|
-
const ssrNodes = getAllSsrPropertyNodes();
|
|
5401
|
-
if (ssrNodes.length > 0) {
|
|
5402
|
-
const propsData = {};
|
|
5403
|
-
for (let i = 0; i < ssrNodes.length; i++) {
|
|
5404
|
-
const node = ssrNodes[i];
|
|
5405
|
-
const entries = getSsrProperties(node);
|
|
5406
|
-
if (entries.length === 0)
|
|
5407
|
-
continue;
|
|
5408
|
-
const id = `wcs-ssr-${i}`;
|
|
5409
|
-
node.setAttribute('data-wcs-ssr-id', id);
|
|
5410
|
-
const props = {};
|
|
5411
|
-
for (const entry of entries) {
|
|
5412
|
-
props[entry.propName] = entry.value;
|
|
5413
|
-
}
|
|
5414
|
-
propsData[id] = props;
|
|
5415
|
-
}
|
|
5416
|
-
if (Object.keys(propsData).length > 0) {
|
|
5417
|
-
const propsScript = document.createElement('script');
|
|
5418
|
-
propsScript.setAttribute('type', 'application/json');
|
|
5419
|
-
propsScript.setAttribute('data-wcs-ssr-props', '');
|
|
5420
|
-
propsScript.textContent = escapeJsonForScript(JSON.stringify(propsData));
|
|
5421
|
-
ssrEl.appendChild(propsScript);
|
|
5599
|
+
for (const [propName, value] of Object.entries(propMap)) {
|
|
5600
|
+
target[propName] = value;
|
|
5422
5601
|
}
|
|
5423
5602
|
}
|
|
5424
|
-
clearSsrPropertyStore();
|
|
5425
5603
|
}
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5604
|
+
// ハイドレーション中の重複登録防止用属性を除去
|
|
5605
|
+
const completedEls = root.querySelectorAll('[data-wcs-completed]');
|
|
5606
|
+
for (const el of completedEls) {
|
|
5607
|
+
el.removeAttribute('data-wcs-completed');
|
|
5608
|
+
}
|
|
5609
|
+
return true;
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
const stateElementByNameByNode = new WeakMap();
|
|
5613
|
+
const bindingsReadyByNode = new WeakMap();
|
|
5614
|
+
// devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
|
|
5615
|
+
// サイズは <wcs-state> 要素数に拘束され、unregister(disconnectedCallback)で
|
|
5616
|
+
// 必ず削除されるためリークしない。
|
|
5617
|
+
const liveStateElements = new Set();
|
|
5618
|
+
function getLiveStateElements() {
|
|
5619
|
+
return liveStateElements;
|
|
5620
|
+
}
|
|
5621
|
+
function getStateElementByName(rootNode, name) {
|
|
5622
|
+
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
5623
|
+
if (!stateElementByName) {
|
|
5624
|
+
return null;
|
|
5625
|
+
}
|
|
5626
|
+
return stateElementByName.get(name) || null;
|
|
5627
|
+
}
|
|
5628
|
+
/**
|
|
5629
|
+
* 指定された rootNode のバインディング初期化が完了するまで待機する Promise を返す。
|
|
5630
|
+
*/
|
|
5631
|
+
function getBindingsReady(rootNode) {
|
|
5632
|
+
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
5633
|
+
}
|
|
5634
|
+
function setStateElementByName(rootNode, name, element) {
|
|
5635
|
+
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
5636
|
+
if (element === null) {
|
|
5637
|
+
// 削除の場合、Mapが存在しない場合は何もしない
|
|
5638
|
+
if (!stateElementByName) {
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
5641
|
+
const removed = stateElementByName.get(name);
|
|
5642
|
+
stateElementByName.delete(name);
|
|
5643
|
+
if (stateElementByName.size === 0) {
|
|
5644
|
+
stateElementByNameByNode.delete(rootNode);
|
|
5645
|
+
}
|
|
5646
|
+
if (removed !== undefined) {
|
|
5647
|
+
liveStateElements.delete(removed);
|
|
5648
|
+
if (devtoolsSink !== null) {
|
|
5649
|
+
devtoolsSink({ type: "state:element-unregistered", name, rootNode, element: removed });
|
|
5436
5650
|
}
|
|
5437
5651
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5652
|
+
if (config.debug) {
|
|
5653
|
+
console.debug(`State element unregistered: name="${name}"`);
|
|
5440
5654
|
}
|
|
5441
5655
|
}
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
const
|
|
5450
|
-
if (
|
|
5451
|
-
|
|
5656
|
+
else {
|
|
5657
|
+
// 登録の場合
|
|
5658
|
+
if (!stateElementByName) {
|
|
5659
|
+
stateElementByName = new Map();
|
|
5660
|
+
stateElementByNameByNode.set(rootNode, stateElementByName);
|
|
5661
|
+
// 初めてルートノードに登録する場合
|
|
5662
|
+
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
5663
|
+
const enableSsr = !inSsr() && element.hasAttribute?.('enable-ssr');
|
|
5664
|
+
if (rootNode.constructor.name === 'HTMLDocument' || rootNode.constructor.name === 'Document') {
|
|
5665
|
+
const ready = new Promise((resolve) => {
|
|
5666
|
+
queueMicrotask(async () => {
|
|
5667
|
+
if (enableSsr) {
|
|
5668
|
+
const success = await hydrateBindings(rootNode);
|
|
5669
|
+
if (!success) {
|
|
5670
|
+
await buildBindings(rootNode);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
else {
|
|
5674
|
+
await buildBindings(rootNode);
|
|
5675
|
+
}
|
|
5676
|
+
resolve();
|
|
5677
|
+
});
|
|
5678
|
+
});
|
|
5679
|
+
bindingsReadyByNode.set(rootNode, ready);
|
|
5680
|
+
}
|
|
5681
|
+
else if (rootNode.constructor.name === 'ShadowRoot') {
|
|
5682
|
+
const ready = new Promise((resolve) => {
|
|
5683
|
+
queueMicrotask(async () => {
|
|
5684
|
+
await buildBindings(rootNode);
|
|
5685
|
+
resolve();
|
|
5686
|
+
});
|
|
5687
|
+
});
|
|
5688
|
+
bindingsReadyByNode.set(rootNode, ready);
|
|
5452
5689
|
}
|
|
5453
5690
|
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5691
|
+
if (stateElementByName.has(name)) {
|
|
5692
|
+
raiseError(`State element with name "${name}" is already registered.`);
|
|
5693
|
+
}
|
|
5694
|
+
stateElementByName.set(name, element);
|
|
5695
|
+
liveStateElements.add(element);
|
|
5696
|
+
if (devtoolsSink !== null) {
|
|
5697
|
+
devtoolsSink({ type: "state:element-registered", name, rootNode, element });
|
|
5698
|
+
}
|
|
5699
|
+
if (config.debug) {
|
|
5700
|
+
console.debug(`State element registered: name="${name}"`, element);
|
|
5456
5701
|
}
|
|
5457
5702
|
}
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5703
|
+
}
|
|
5704
|
+
|
|
5705
|
+
const updateBatchListeners = new Set();
|
|
5706
|
+
/**
|
|
5707
|
+
* drain 終了リスナーを登録する。
|
|
5708
|
+
*/
|
|
5709
|
+
function registerUpdateBatchListener(listener) {
|
|
5710
|
+
updateBatchListeners.add(listener);
|
|
5711
|
+
}
|
|
5712
|
+
/**
|
|
5713
|
+
* drain 終了リスナーを解除する(テスト間の分離用)。
|
|
5714
|
+
*/
|
|
5715
|
+
function unregisterUpdateBatchListener(listener) {
|
|
5716
|
+
updateBatchListeners.delete(listener);
|
|
5717
|
+
}
|
|
5718
|
+
/**
|
|
5719
|
+
* 全リスナーに drain のバッチを通知する。
|
|
5720
|
+
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
5721
|
+
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
5722
|
+
*/
|
|
5723
|
+
function notifyUpdateBatchListeners(batch) {
|
|
5724
|
+
for (const listener of updateBatchListeners) {
|
|
5725
|
+
listener(batch);
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
class Updater {
|
|
5729
|
+
_queueAbsoluteAddresses = [];
|
|
5730
|
+
constructor() {
|
|
5731
|
+
}
|
|
5732
|
+
enqueueAbsoluteAddress(absoluteAddress) {
|
|
5733
|
+
const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
|
|
5734
|
+
this._queueAbsoluteAddresses.push(absoluteAddress);
|
|
5735
|
+
if (requireStartProcess) {
|
|
5736
|
+
queueMicrotask(() => {
|
|
5737
|
+
const absoluteAddresses = this._queueAbsoluteAddresses;
|
|
5738
|
+
this._queueAbsoluteAddresses = [];
|
|
5739
|
+
this._applyChange(absoluteAddresses);
|
|
5740
|
+
});
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
// テスト用に公開
|
|
5744
|
+
testApplyChange(absoluteAddresses) {
|
|
5745
|
+
this._applyChange(absoluteAddresses);
|
|
5746
|
+
}
|
|
5747
|
+
_applyChange(absoluteAddresses) {
|
|
5748
|
+
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
5749
|
+
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
5750
|
+
// Set による重複排除が正しく機能する
|
|
5751
|
+
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
5752
|
+
const processBindings = [];
|
|
5753
|
+
for (const absoluteAddress of absoluteAddressSet) {
|
|
5754
|
+
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
5755
|
+
// アドレス等)に空 Set を生成・蓄積しない
|
|
5756
|
+
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
5757
|
+
if (bindings === undefined) {
|
|
5758
|
+
continue;
|
|
5471
5759
|
}
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
let sibling = comment.nextSibling;
|
|
5477
|
-
comment.remove();
|
|
5478
|
-
const endPattern = `@@wcs-text-end:${path}`;
|
|
5479
|
-
while (sibling) {
|
|
5480
|
-
const next = sibling.nextSibling;
|
|
5481
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5482
|
-
sibling.parentNode.removeChild(sibling);
|
|
5483
|
-
break;
|
|
5760
|
+
for (const binding of bindings) {
|
|
5761
|
+
if (binding.replaceNode.isConnected === false) {
|
|
5762
|
+
// 切断されているバインディングは無視
|
|
5763
|
+
continue;
|
|
5484
5764
|
}
|
|
5485
|
-
|
|
5486
|
-
sibling = next;
|
|
5765
|
+
processBindings.push(binding);
|
|
5487
5766
|
}
|
|
5488
5767
|
}
|
|
5768
|
+
applyChangeFromBindings(processBindings);
|
|
5769
|
+
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
5770
|
+
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
5771
|
+
notifyUpdateBatchListeners(absoluteAddressSet);
|
|
5489
5772
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5773
|
+
}
|
|
5774
|
+
const updater = new Updater();
|
|
5775
|
+
function getUpdater() {
|
|
5776
|
+
return updater;
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
/**
|
|
5780
|
+
* devtools/types.ts
|
|
5781
|
+
*
|
|
5782
|
+
* DevTools Hook Protocol (docs/devtools-hook-protocol.md) の型定義。
|
|
5783
|
+
*
|
|
5784
|
+
* イベント payload はランタイム内部オブジェクト(IAbsoluteStateAddress /
|
|
5785
|
+
* IBindingInfo 等)への生参照を含む(同一 realm・オーバーレイ前提、protocol 原則 4)。
|
|
5786
|
+
* 消費者はこれらを変異してはならない。
|
|
5787
|
+
*/
|
|
5788
|
+
/** グローバル registry のプロパティ名 */
|
|
5789
|
+
const DEVTOOLS_HOOK_GLOBAL = "__WCSTACK_DEVTOOLS_HOOK__";
|
|
5790
|
+
/** プロトコル版。additive change では上げない(protocol §2) */
|
|
5791
|
+
const DEVTOOLS_PROTOCOL_VERSION = 1;
|
|
5792
|
+
|
|
5793
|
+
/**
|
|
5794
|
+
* devtools/bridge.ts
|
|
5795
|
+
*
|
|
5796
|
+
* DevTools Hook Protocol (docs/devtools-hook-protocol.md) の state 側実装。
|
|
5797
|
+
*
|
|
5798
|
+
* - registry 最小実装: `globalThis.__WCSTACK_DEVTOOLS_HOOK__` を create-if-missing で
|
|
5799
|
+
* 確保する(ロード順非依存・先勝ち。devtools 側 client も同一仕様の実装を持つ)。
|
|
5800
|
+
* - source: この state モジュールコピーを 1 source として登録する。同一ページに
|
|
5801
|
+
* コピーが複数あれば複数 source になる(正常系、protocol §5)。
|
|
5802
|
+
* - sink 切替: listener の有無に応じて registry が `_setSink` を呼び、ここで
|
|
5803
|
+
* updater の drain リスナー登録/解除も連動させる(protocol §4.3)。
|
|
5804
|
+
*/
|
|
5805
|
+
/**
|
|
5806
|
+
* registry の最小実装(protocol §2)。30 行程度に抑え、振る舞いは
|
|
5807
|
+
* 「source/listener の管理と sink の配線」のみ。台帳・整形は devtools 側の責務。
|
|
5808
|
+
*/
|
|
5809
|
+
function createMinimalRegistry() {
|
|
5810
|
+
const sources = new Map();
|
|
5811
|
+
const listeners = new Set();
|
|
5812
|
+
const applySink = (source) => {
|
|
5813
|
+
if (listeners.size === 0) {
|
|
5814
|
+
source._setSink(null);
|
|
5815
|
+
return;
|
|
5513
5816
|
}
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
const comment = walker1.currentNode;
|
|
5519
|
-
if (SSR_BLOCK_START.test(comment.data)) {
|
|
5520
|
-
startComments.push(comment);
|
|
5817
|
+
const sourceId = source.id;
|
|
5818
|
+
source._setSink((event) => {
|
|
5819
|
+
for (const listener of listeners) {
|
|
5820
|
+
listener.onEvent?.(sourceId, event);
|
|
5521
5821
|
}
|
|
5522
|
-
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
const next = sibling.nextSibling;
|
|
5531
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5532
|
-
sibling.remove();
|
|
5533
|
-
break;
|
|
5534
|
-
}
|
|
5535
|
-
sibling.remove();
|
|
5536
|
-
sibling = next;
|
|
5822
|
+
});
|
|
5823
|
+
};
|
|
5824
|
+
return {
|
|
5825
|
+
version: DEVTOOLS_PROTOCOL_VERSION,
|
|
5826
|
+
sources,
|
|
5827
|
+
register(source) {
|
|
5828
|
+
if (sources.has(source.id)) {
|
|
5829
|
+
return;
|
|
5537
5830
|
}
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
// プレースホルダーコメント (@@wcs-for:uuid 等) をテンプレートに差し替え
|
|
5543
|
-
const walker2 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
5544
|
-
const placeholders = [];
|
|
5545
|
-
while (walker2.nextNode()) {
|
|
5546
|
-
const comment = walker2.currentNode;
|
|
5547
|
-
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
5548
|
-
const uuid = comment.data.split(':')[1];
|
|
5549
|
-
placeholders.push({ comment, uuid });
|
|
5831
|
+
sources.set(source.id, source);
|
|
5832
|
+
applySink(source);
|
|
5833
|
+
for (const listener of listeners) {
|
|
5834
|
+
listener.onSourceRegistered?.(source);
|
|
5550
5835
|
}
|
|
5551
|
-
}
|
|
5552
|
-
|
|
5553
|
-
const
|
|
5554
|
-
if (
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5836
|
+
},
|
|
5837
|
+
unregister(sourceId) {
|
|
5838
|
+
const source = sources.get(sourceId);
|
|
5839
|
+
if (source === undefined) {
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
source._setSink(null);
|
|
5843
|
+
sources.delete(sourceId);
|
|
5844
|
+
for (const listener of listeners) {
|
|
5845
|
+
listener.onSourceUnregistered?.(sourceId);
|
|
5846
|
+
}
|
|
5847
|
+
},
|
|
5848
|
+
addListener(listener) {
|
|
5849
|
+
listeners.add(listener);
|
|
5850
|
+
// 既登録 source をリプレイ(遅延アタッチの起点、protocol §6)
|
|
5851
|
+
for (const source of sources.values()) {
|
|
5852
|
+
applySink(source);
|
|
5853
|
+
listener.onSourceRegistered?.(source);
|
|
5854
|
+
}
|
|
5855
|
+
return () => {
|
|
5856
|
+
if (!listeners.delete(listener)) {
|
|
5857
|
+
return;
|
|
5562
5858
|
}
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
restored.content.appendChild(document.importNode(child, true));
|
|
5566
|
-
}
|
|
5859
|
+
for (const source of sources.values()) {
|
|
5860
|
+
applySink(source);
|
|
5567
5861
|
}
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5862
|
+
};
|
|
5863
|
+
},
|
|
5864
|
+
};
|
|
5865
|
+
}
|
|
5866
|
+
function getOrCreateHookRegistry() {
|
|
5867
|
+
const globals = globalThis;
|
|
5868
|
+
const existing = globals[DEVTOOLS_HOOK_GLOBAL];
|
|
5869
|
+
if (existing !== undefined) {
|
|
5870
|
+
if (existing.version !== DEVTOOLS_PROTOCOL_VERSION) {
|
|
5871
|
+
// 先勝ち固定。振る舞いは差し替えない(protocol §2)
|
|
5872
|
+
console.warn(`[wcstack/state] devtools hook registry version mismatch: found ${existing.version}, expected ${DEVTOOLS_PROTOCOL_VERSION}. Keeping the existing registry (first-wins).`);
|
|
5579
5873
|
}
|
|
5874
|
+
return existing;
|
|
5875
|
+
}
|
|
5876
|
+
const registry = createMinimalRegistry();
|
|
5877
|
+
globals[DEVTOOLS_HOOK_GLOBAL] = registry;
|
|
5878
|
+
return registry;
|
|
5879
|
+
}
|
|
5880
|
+
/**
|
|
5881
|
+
* drain 終了バッチの転送リスナー。sink 接続中のみ updater に登録される。
|
|
5882
|
+
*/
|
|
5883
|
+
const onUpdateBatch = (batch) => {
|
|
5884
|
+
if (devtoolsSink !== null) {
|
|
5885
|
+
devtoolsSink({ type: "state:update-batch", addresses: batch });
|
|
5886
|
+
}
|
|
5887
|
+
};
|
|
5888
|
+
/**
|
|
5889
|
+
* registry からの sink 差し替え。updater の drain リスナー登録/解除を連動させる。
|
|
5890
|
+
* detach 時に登録が残らないこと(protocol §7-2)。
|
|
5891
|
+
*/
|
|
5892
|
+
function setSink(sink) {
|
|
5893
|
+
const wasActive = devtoolsSink !== null;
|
|
5894
|
+
setDevtoolsSink(sink);
|
|
5895
|
+
const isActive = sink !== null;
|
|
5896
|
+
if (isActive && !wasActive) {
|
|
5897
|
+
registerUpdateBatchListener(onUpdateBatch);
|
|
5898
|
+
}
|
|
5899
|
+
else if (!isActive && wasActive) {
|
|
5900
|
+
unregisterUpdateBatchListener(onUpdateBatch);
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
function createStateElementSummary(element) {
|
|
5904
|
+
return {
|
|
5905
|
+
name: element.name,
|
|
5906
|
+
rootNode: element.rootNode,
|
|
5907
|
+
element,
|
|
5908
|
+
paths: {
|
|
5909
|
+
list: element.listPaths,
|
|
5910
|
+
element: element.elementPaths,
|
|
5911
|
+
getter: element.getterPaths,
|
|
5912
|
+
setter: element.setterPaths,
|
|
5913
|
+
},
|
|
5914
|
+
commandTokenNames: element.commandTokenNames,
|
|
5915
|
+
eventTokenNames: element.eventTokenNames,
|
|
5916
|
+
staticDependency: element.staticDependency,
|
|
5917
|
+
dynamicDependency: element.dynamicDependency,
|
|
5918
|
+
};
|
|
5919
|
+
}
|
|
5920
|
+
function requireStateElement(name, rootNode) {
|
|
5921
|
+
return getStateElementByName(rootNode, name) ??
|
|
5922
|
+
raiseError(`devtools: state element not found: name="${name}"`);
|
|
5923
|
+
}
|
|
5924
|
+
function createSourceId() {
|
|
5925
|
+
// getUUID() はモジュールローカル連番のため、state コピーが複数ある
|
|
5926
|
+
// ページで source id が衝突する。ランダム採番で回避する。
|
|
5927
|
+
return "state:" + Math.random().toString(36).slice(2, 10);
|
|
5928
|
+
}
|
|
5929
|
+
let registeredSource = null;
|
|
5930
|
+
/**
|
|
5931
|
+
* この state ランタイムを 1 source として registry に登録する。
|
|
5932
|
+
* bootstrapState() から呼ばれる。冪等・SSR では何もしない(protocol 原則 6)。
|
|
5933
|
+
*/
|
|
5934
|
+
function registerDevtoolsSource() {
|
|
5935
|
+
if (inSsr()) {
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
if (registeredSource !== null) {
|
|
5939
|
+
return;
|
|
5580
5940
|
}
|
|
5941
|
+
const source = {
|
|
5942
|
+
id: createSourceId(),
|
|
5943
|
+
kind: "state",
|
|
5944
|
+
packageVersion: VERSION,
|
|
5945
|
+
getStateElements() {
|
|
5946
|
+
const summaries = [];
|
|
5947
|
+
for (const element of getLiveStateElements()) {
|
|
5948
|
+
summaries.push(createStateElementSummary(element));
|
|
5949
|
+
}
|
|
5950
|
+
return summaries;
|
|
5951
|
+
},
|
|
5952
|
+
keys(name, rootNode) {
|
|
5953
|
+
const element = requireStateElement(name, rootNode);
|
|
5954
|
+
const result = [];
|
|
5955
|
+
element.createState("readonly", (state) => {
|
|
5956
|
+
// Object.keys は Proxy の ownKeys 経由で target の own key を返す。
|
|
5957
|
+
// メソッド判別の typeof アクセスは getter を 1 回実行する副作用があるため、
|
|
5958
|
+
// ループ文脈依存で throw する getter は catch して「キーとしては存在する」
|
|
5959
|
+
// 側に倒す(値の表示可否は UI 側の責務)。
|
|
5960
|
+
for (const key of Object.keys(state)) {
|
|
5961
|
+
if (key.includes("*") || key.startsWith("$")) {
|
|
5962
|
+
continue;
|
|
5963
|
+
}
|
|
5964
|
+
try {
|
|
5965
|
+
if (typeof state[key] === "function") {
|
|
5966
|
+
continue;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
catch {
|
|
5970
|
+
// 読めない getter もキーとしては列挙する
|
|
5971
|
+
}
|
|
5972
|
+
result.push(key);
|
|
5973
|
+
}
|
|
5974
|
+
});
|
|
5975
|
+
return result;
|
|
5976
|
+
},
|
|
5977
|
+
read(name, rootNode, path, indexes) {
|
|
5978
|
+
const element = requireStateElement(name, rootNode);
|
|
5979
|
+
let result;
|
|
5980
|
+
element.createState("readonly", (state) => {
|
|
5981
|
+
result = state["$resolve"](path, indexes ?? []);
|
|
5982
|
+
});
|
|
5983
|
+
return result;
|
|
5984
|
+
},
|
|
5985
|
+
write(name, rootNode, path, value, indexes) {
|
|
5986
|
+
const element = requireStateElement(name, rootNode);
|
|
5987
|
+
element.createState("writable", (state) => {
|
|
5988
|
+
if (indexes !== undefined && indexes.length > 0) {
|
|
5989
|
+
// Note: $resolve は value===undefined を「取得」と解釈するため、
|
|
5990
|
+
// ワイルドカードパスへの undefined 書き込みは非サポート
|
|
5991
|
+
// (spread undefined 規範と同じ側に倒す)
|
|
5992
|
+
state["$resolve"](path, indexes, value);
|
|
5993
|
+
}
|
|
5994
|
+
else {
|
|
5995
|
+
state[path] = value;
|
|
5996
|
+
}
|
|
5997
|
+
});
|
|
5998
|
+
},
|
|
5999
|
+
_setSink: setSink,
|
|
6000
|
+
};
|
|
6001
|
+
registeredSource = source;
|
|
6002
|
+
getOrCreateHookRegistry().register(source);
|
|
5581
6003
|
}
|
|
5582
6004
|
|
|
5583
6005
|
async function loadFromInnerScript(script, name) {
|
|
@@ -5666,8 +6088,14 @@ class LoopContextStack {
|
|
|
5666
6088
|
}
|
|
5667
6089
|
}
|
|
5668
6090
|
else {
|
|
5669
|
-
|
|
5670
|
-
|
|
6091
|
+
// With no active loop context the address must be self-contained: the
|
|
6092
|
+
// listIndex chain supplies one index per wildcard. Top-level lists
|
|
6093
|
+
// (wildcardCount 1) always satisfy this. A nested list re-rendered
|
|
6094
|
+
// directly (e.g. replaced via $resolve from outside the loop) also
|
|
6095
|
+
// satisfies it — the for binding's listIndex carries the full ancestor
|
|
6096
|
+
// chain.
|
|
6097
|
+
if (loopContext.listIndex.length !== loopContext.pathInfo.wildcardCount) {
|
|
6098
|
+
raiseError(`Cannot push loop context when there is no active loop context: the list index chain (length ${loopContext.listIndex.length}) does not cover the wildcard path (wildcard count ${loopContext.pathInfo.wildcardCount}).`);
|
|
5671
6099
|
}
|
|
5672
6100
|
}
|
|
5673
6101
|
this._loopContextStack[this._length] = loopContext;
|
|
@@ -5739,7 +6167,7 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
5739
6167
|
}
|
|
5740
6168
|
let token = registry.get(name);
|
|
5741
6169
|
if (typeof token === "undefined") {
|
|
5742
|
-
token = new CommandToken(name);
|
|
6170
|
+
token = new CommandToken(name, stateElement.name);
|
|
5743
6171
|
registry.set(name, token);
|
|
5744
6172
|
}
|
|
5745
6173
|
return token;
|
|
@@ -6259,74 +6687,6 @@ function clearStreamNamespace(stateElement) {
|
|
|
6259
6687
|
errorNamespaceByStateElement.delete(stateElement);
|
|
6260
6688
|
}
|
|
6261
6689
|
|
|
6262
|
-
const updateBatchListeners = new Set();
|
|
6263
|
-
/**
|
|
6264
|
-
* drain 終了リスナーを登録する。
|
|
6265
|
-
*/
|
|
6266
|
-
function registerUpdateBatchListener(listener) {
|
|
6267
|
-
updateBatchListeners.add(listener);
|
|
6268
|
-
}
|
|
6269
|
-
/**
|
|
6270
|
-
* 全リスナーに drain のバッチを通知する。
|
|
6271
|
-
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
6272
|
-
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
6273
|
-
*/
|
|
6274
|
-
function notifyUpdateBatchListeners(batch) {
|
|
6275
|
-
for (const listener of updateBatchListeners) {
|
|
6276
|
-
listener(batch);
|
|
6277
|
-
}
|
|
6278
|
-
}
|
|
6279
|
-
class Updater {
|
|
6280
|
-
_queueAbsoluteAddresses = [];
|
|
6281
|
-
constructor() {
|
|
6282
|
-
}
|
|
6283
|
-
enqueueAbsoluteAddress(absoluteAddress) {
|
|
6284
|
-
const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
|
|
6285
|
-
this._queueAbsoluteAddresses.push(absoluteAddress);
|
|
6286
|
-
if (requireStartProcess) {
|
|
6287
|
-
queueMicrotask(() => {
|
|
6288
|
-
const absoluteAddresses = this._queueAbsoluteAddresses;
|
|
6289
|
-
this._queueAbsoluteAddresses = [];
|
|
6290
|
-
this._applyChange(absoluteAddresses);
|
|
6291
|
-
});
|
|
6292
|
-
}
|
|
6293
|
-
}
|
|
6294
|
-
// テスト用に公開
|
|
6295
|
-
testApplyChange(absoluteAddresses) {
|
|
6296
|
-
this._applyChange(absoluteAddresses);
|
|
6297
|
-
}
|
|
6298
|
-
_applyChange(absoluteAddresses) {
|
|
6299
|
-
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
6300
|
-
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
6301
|
-
// Set による重複排除が正しく機能する
|
|
6302
|
-
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6303
|
-
const processBindings = [];
|
|
6304
|
-
for (const absoluteAddress of absoluteAddressSet) {
|
|
6305
|
-
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
6306
|
-
// アドレス等)に空 Set を生成・蓄積しない
|
|
6307
|
-
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6308
|
-
if (bindings === undefined) {
|
|
6309
|
-
continue;
|
|
6310
|
-
}
|
|
6311
|
-
for (const binding of bindings) {
|
|
6312
|
-
if (binding.replaceNode.isConnected === false) {
|
|
6313
|
-
// 切断されているバインディングは無視
|
|
6314
|
-
continue;
|
|
6315
|
-
}
|
|
6316
|
-
processBindings.push(binding);
|
|
6317
|
-
}
|
|
6318
|
-
}
|
|
6319
|
-
applyChangeFromBindings(processBindings);
|
|
6320
|
-
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
6321
|
-
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
6322
|
-
notifyUpdateBatchListeners(absoluteAddressSet);
|
|
6323
|
-
}
|
|
6324
|
-
}
|
|
6325
|
-
const updater = new Updater();
|
|
6326
|
-
function getUpdater() {
|
|
6327
|
-
return updater;
|
|
6328
|
-
}
|
|
6329
|
-
|
|
6330
6690
|
/**
|
|
6331
6691
|
* stream/argsTrace.ts
|
|
6332
6692
|
*
|
|
@@ -7616,11 +7976,17 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
7616
7976
|
// primitive 値かつ Object.is 同値なら、set / enqueue / walkDependency / DOM 適用 /
|
|
7617
7977
|
// $updatedCallback / DCC イベントを丸ごとスキップ(標準的なリアクティブ no-op)。
|
|
7618
7978
|
// 参照型(object/array)は in-place mutation 取りこぼし防止のため素通し(ガードしない)。
|
|
7979
|
+
// devtools write イベント用: guard が既に取得した旧値のみ流用する
|
|
7980
|
+
// (参照型のために追加の get はしない — protocol §4.2)
|
|
7981
|
+
let devOldValue;
|
|
7982
|
+
let devHasOldValue = false;
|
|
7619
7983
|
if (config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
7620
7984
|
const oldValue = getByAddress(target, address, receiver, handler);
|
|
7621
7985
|
if (Object.is(oldValue, value)) {
|
|
7622
7986
|
return true;
|
|
7623
7987
|
}
|
|
7988
|
+
devOldValue = oldValue;
|
|
7989
|
+
devHasOldValue = true;
|
|
7624
7990
|
}
|
|
7625
7991
|
// --- end same-value guard ---
|
|
7626
7992
|
const isSwappable = stateElement.elementPaths.has(address.pathInfo.path);
|
|
@@ -7628,6 +7994,15 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
7628
7994
|
stateElement.getterPaths.has(address.pathInfo.path);
|
|
7629
7995
|
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
7630
7996
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
7997
|
+
if (devtoolsSink !== null) {
|
|
7998
|
+
devtoolsSink({
|
|
7999
|
+
type: "state:write",
|
|
8000
|
+
absoluteAddress: absAddress,
|
|
8001
|
+
value,
|
|
8002
|
+
oldValue: devOldValue,
|
|
8003
|
+
hasOldValue: devHasOldValue,
|
|
8004
|
+
});
|
|
8005
|
+
}
|
|
7631
8006
|
try {
|
|
7632
8007
|
if (isSwappable) {
|
|
7633
8008
|
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler);
|
|
@@ -8002,15 +8377,16 @@ async function setLoopContextAsync(handler, loopContext, callback) {
|
|
|
8002
8377
|
* StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
|
|
8003
8378
|
*
|
|
8004
8379
|
* 主な役割:
|
|
8005
|
-
* - 文字列プロパティの場合、特殊プロパティ($1
|
|
8380
|
+
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
|
|
8381
|
+
* $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
|
|
8006
8382
|
* - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
|
|
8007
8383
|
* - getByRefで構造化パス・リストインデックスに対応した値を取得
|
|
8008
8384
|
* - シンボルプロパティの場合はhandler.callableApi経由でAPIを呼び出し
|
|
8009
8385
|
* - それ以外はReflect.getで通常のプロパティアクセスを実行
|
|
8010
8386
|
*
|
|
8011
8387
|
* 設計ポイント:
|
|
8012
|
-
* - $1〜$
|
|
8013
|
-
* - $
|
|
8388
|
+
* - $1〜$128(MAX_WILDCARD_DEPTH)は直近のStatePropertyRefのリストインデックス値を返す特殊プロパティ
|
|
8389
|
+
* - $getAll, $resolve 等はAPI関数を、$command / $streamStatus / $streamError は名前空間を返す
|
|
8014
8390
|
* - 通常のプロパティアクセスもバインディングや多重ループに対応
|
|
8015
8391
|
* - シンボルAPIやReflect.getで拡張性・互換性も確保
|
|
8016
8392
|
*/
|
|
@@ -9183,6 +9559,8 @@ function bootstrapState(config) {
|
|
|
9183
9559
|
setConfig(config);
|
|
9184
9560
|
}
|
|
9185
9561
|
registerComponents();
|
|
9562
|
+
// DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
|
|
9563
|
+
registerDevtoolsSource();
|
|
9186
9564
|
}
|
|
9187
9565
|
|
|
9188
9566
|
/**
|
|
@@ -9382,6 +9760,9 @@ function getWcsManifest() {
|
|
|
9382
9760
|
STATE_COMMAND_NAMESPACE_NAME,
|
|
9383
9761
|
STATE_EVENT_TOKENS_NAME,
|
|
9384
9762
|
STATE_ON_NAME,
|
|
9763
|
+
STATE_STREAMS_NAME,
|
|
9764
|
+
STATE_STREAM_STATUS_NAMESPACE_NAME,
|
|
9765
|
+
STATE_STREAM_ERROR_NAMESPACE_NAME,
|
|
9385
9766
|
],
|
|
9386
9767
|
};
|
|
9387
9768
|
}
|