@wcstack/state 1.19.1 → 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 +1152 -813
- 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
|
@@ -65,16 +65,6 @@ function setConfig(partialConfig) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
var version$1 = "1.19.1";
|
|
69
|
-
var pkg = {
|
|
70
|
-
version: version$1};
|
|
71
|
-
|
|
72
|
-
const VERSION = pkg.version;
|
|
73
|
-
|
|
74
|
-
function raiseError(message) {
|
|
75
|
-
throw new Error(`[@wcstack/state] ${message}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
68
|
const bindingPromiseByNode = new WeakMap();
|
|
79
69
|
let id$1 = 0;
|
|
80
70
|
function getInitializeBindingPromiseByNode(node) {
|
|
@@ -103,6 +93,10 @@ function resolveInitializedBinding(node) {
|
|
|
103
93
|
bindingPromise.resolve();
|
|
104
94
|
}
|
|
105
95
|
|
|
96
|
+
function raiseError(message) {
|
|
97
|
+
throw new Error(`[@wcstack/state] ${message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
106
100
|
function replaceToReplaceNode(bindingInfo) {
|
|
107
101
|
const node = bindingInfo.node;
|
|
108
102
|
const replaceNode = bindingInfo.replaceNode;
|
|
@@ -1593,6 +1587,37 @@ function parseBindTextForEmbeddedNode(bindText) {
|
|
|
1593
1587
|
};
|
|
1594
1588
|
}
|
|
1595
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
|
+
|
|
1596
1621
|
function getParseBindTextResults(node) {
|
|
1597
1622
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1598
1623
|
const element = node;
|
|
@@ -1775,6 +1800,23 @@ function createStateAddress(pathInfo, listIndex) {
|
|
|
1775
1800
|
}
|
|
1776
1801
|
}
|
|
1777
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
|
+
|
|
1778
1820
|
// command-token / event-token が共有する pub/sub プリミティブ。
|
|
1779
1821
|
// _subscribers は Set のため挿入順を保持する。
|
|
1780
1822
|
// emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
|
|
@@ -1814,7 +1856,31 @@ class Token {
|
|
|
1814
1856
|
|
|
1815
1857
|
// CommandToken は共有 pub/sub プリミティブ Token の薄い特化。
|
|
1816
1858
|
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
1859
|
+
//
|
|
1860
|
+
// ownerStateName は devtools 計装(protocol §4.5)のための内部 optional 引数。
|
|
1861
|
+
// command-token-protocol の外部仕様は不変更(registry が渡すだけで、
|
|
1862
|
+
// subscribe/emit の意味論には一切影響しない)。
|
|
1817
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
|
+
}
|
|
1818
1884
|
}
|
|
1819
1885
|
function isCommandToken(value) {
|
|
1820
1886
|
return value instanceof CommandToken;
|
|
@@ -1958,7 +2024,28 @@ function attachEventHandler(binding) {
|
|
|
1958
2024
|
|
|
1959
2025
|
// EventToken は共有 pub/sub プリミティブ Token の薄い特化(element→state 方向)。
|
|
1960
2026
|
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
2027
|
+
//
|
|
2028
|
+
// ownerStateName は devtools 計装(protocol §4.5)のための内部 optional 引数。
|
|
2029
|
+
// event-token-protocol の外部仕様は不変更。
|
|
1961
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
|
+
}
|
|
1962
2049
|
}
|
|
1963
2050
|
|
|
1964
2051
|
const registryByStateElement$2 = new WeakMap();
|
|
@@ -1970,7 +2057,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
1970
2057
|
}
|
|
1971
2058
|
let token = registry.get(name);
|
|
1972
2059
|
if (typeof token === "undefined") {
|
|
1973
|
-
token = new EventToken(name);
|
|
2060
|
+
token = new EventToken(name, stateElement.name);
|
|
1974
2061
|
registry.set(name, token);
|
|
1975
2062
|
}
|
|
1976
2063
|
return token;
|
|
@@ -2606,7 +2693,7 @@ function getUUID() {
|
|
|
2606
2693
|
return `u${(count++).toString(36)}`;
|
|
2607
2694
|
}
|
|
2608
2695
|
|
|
2609
|
-
let version = 0;
|
|
2696
|
+
let version$1 = 0;
|
|
2610
2697
|
class ListIndex {
|
|
2611
2698
|
uuid = getUUID();
|
|
2612
2699
|
parentListIndex;
|
|
@@ -2627,7 +2714,7 @@ class ListIndex {
|
|
|
2627
2714
|
this.position = parentListIndex ? parentListIndex.position + 1 : 0;
|
|
2628
2715
|
this.length = this.position + 1;
|
|
2629
2716
|
this._index = index;
|
|
2630
|
-
this._version = version;
|
|
2717
|
+
this._version = version$1;
|
|
2631
2718
|
}
|
|
2632
2719
|
/**
|
|
2633
2720
|
* Gets current index value.
|
|
@@ -2644,7 +2731,7 @@ class ListIndex {
|
|
|
2644
2731
|
*/
|
|
2645
2732
|
set index(value) {
|
|
2646
2733
|
this._index = value;
|
|
2647
|
-
this._version = ++version;
|
|
2734
|
+
this._version = ++version$1;
|
|
2648
2735
|
this.indexes[this.position] = value;
|
|
2649
2736
|
}
|
|
2650
2737
|
/**
|
|
@@ -2683,7 +2770,7 @@ class ListIndex {
|
|
|
2683
2770
|
else {
|
|
2684
2771
|
if (typeof this._indexes === "undefined" || this.dirty) {
|
|
2685
2772
|
this._indexes = [...this.parentListIndex.indexes, this._index];
|
|
2686
|
-
this._version = version;
|
|
2773
|
+
this._version = version$1;
|
|
2687
2774
|
}
|
|
2688
2775
|
}
|
|
2689
2776
|
return this._indexes;
|
|
@@ -3083,12 +3170,18 @@ function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
|
3083
3170
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
3084
3171
|
const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
|
|
3085
3172
|
bindingSet.add(binding);
|
|
3173
|
+
if (devtoolsSink !== null) {
|
|
3174
|
+
devtoolsSink({ type: "state:binding-added", absoluteAddress: absoluteStateAddress, binding });
|
|
3175
|
+
}
|
|
3086
3176
|
}
|
|
3087
3177
|
function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
3088
3178
|
// get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
|
|
3089
3179
|
const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3090
3180
|
if (bindingSet !== undefined) {
|
|
3091
3181
|
bindingSet.delete(binding);
|
|
3182
|
+
if (devtoolsSink !== null) {
|
|
3183
|
+
devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
|
|
3184
|
+
}
|
|
3092
3185
|
}
|
|
3093
3186
|
}
|
|
3094
3187
|
|
|
@@ -4780,840 +4873,1133 @@ async function buildBindings(root) {
|
|
|
4780
4873
|
}
|
|
4781
4874
|
}
|
|
4782
4875
|
|
|
4783
|
-
|
|
4784
|
-
|
|
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:(.+)$/;
|
|
4785
4887
|
/**
|
|
4786
|
-
*
|
|
4888
|
+
* script 要素へ埋め込む JSON を HTML パーサから保護する。
|
|
4889
|
+
* HTML 直列化時、script の中身は生のまま出力されるため、state 値に
|
|
4890
|
+
* "</script>" や "<!--" を含む文字列があると script を脱出できてしまう。
|
|
4891
|
+
* "<" ">" "&" と U+2028/U+2029 を JSON の \uXXXX エスケープへ置換する
|
|
4892
|
+
* (JSON.parse では元の文字列と等価に復元される)。
|
|
4787
4893
|
*/
|
|
4788
|
-
function
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
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';
|
|
4795
4908
|
}
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
const parts = info.split(':');
|
|
4803
|
-
let uuid;
|
|
4804
|
-
let path;
|
|
4805
|
-
let index = null;
|
|
4806
|
-
if (type === 'for') {
|
|
4807
|
-
// uuid:path:index
|
|
4808
|
-
uuid = parts[0];
|
|
4809
|
-
path = parts[1];
|
|
4810
|
-
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();
|
|
4811
4915
|
}
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4916
|
+
return this._stateData;
|
|
4917
|
+
}
|
|
4918
|
+
get templates() {
|
|
4919
|
+
if (this._templates === null) {
|
|
4920
|
+
this._templates = this._loadTemplates();
|
|
4816
4921
|
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
4823
|
-
break;
|
|
4824
|
-
}
|
|
4825
|
-
nodes.push(sibling);
|
|
4826
|
-
sibling = sibling.nextSibling;
|
|
4922
|
+
return this._templates;
|
|
4923
|
+
}
|
|
4924
|
+
get hydrateProps() {
|
|
4925
|
+
if (this._hydrateProps === null) {
|
|
4926
|
+
this._hydrateProps = this._loadHydrateProps();
|
|
4827
4927
|
}
|
|
4828
|
-
|
|
4928
|
+
return this._hydrateProps;
|
|
4829
4929
|
}
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
/**
|
|
4833
|
-
* live DOM ノード群からバインディングを収集する。
|
|
4834
|
-
* ノードを一時的に DocumentFragment に移動して collectNodesAndBindingInfos を実行し、
|
|
4835
|
-
* 元の位置に戻す。
|
|
4836
|
-
*/
|
|
4837
|
-
function collectBindingsFromLiveNodes(nodes) {
|
|
4838
|
-
if (nodes.length === 0)
|
|
4839
|
-
return { bindingInfos: [], subscriberNodes: [] };
|
|
4840
|
-
// ノードの元の位置を記録
|
|
4841
|
-
const parent = nodes[0].parentNode;
|
|
4842
|
-
const nextSibling = nodes[nodes.length - 1].nextSibling;
|
|
4843
|
-
// 一時的に wrapper 要素に移動(collectNodesAndBindingInfos は Element を受け付ける)
|
|
4844
|
-
const wrapper = document.createElement('div');
|
|
4845
|
-
for (const node of nodes) {
|
|
4846
|
-
wrapper.appendChild(node);
|
|
4930
|
+
getTemplate(uuid) {
|
|
4931
|
+
return this.templates.get(uuid) ?? null;
|
|
4847
4932
|
}
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
if (
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
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];
|
|
4860
4946
|
}
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
while (wrapper.firstChild) {
|
|
4864
|
-
parent.insertBefore(wrapper.firstChild, nextSibling);
|
|
4865
|
-
}
|
|
4947
|
+
setStateData(data) {
|
|
4948
|
+
this._stateData = data;
|
|
4866
4949
|
}
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
// for ブロックの listIndex を UUID ごとに収集
|
|
4877
|
-
const listIndexesByUuid = new Map();
|
|
4878
|
-
for (const block of blocks) {
|
|
4879
|
-
if (block.nodes.length === 0)
|
|
4880
|
-
continue;
|
|
4881
|
-
const content = createContentFromNodes(block.nodes);
|
|
4882
|
-
// Content のバインディングを収集
|
|
4883
|
-
const { bindingInfos, subscriberNodes } = collectBindingsFromLiveNodes(block.nodes);
|
|
4884
|
-
// Content 内のノードに data-wcs-completed を付与
|
|
4885
|
-
// (メインの collectNodesAndBindingInfos で重複登録されないようにする)
|
|
4886
|
-
for (const node of subscriberNodes) {
|
|
4887
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4888
|
-
node.setAttribute('data-wcs-completed', '');
|
|
4889
|
-
}
|
|
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 || '{}');
|
|
4890
4959
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
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);
|
|
4897
4971
|
}
|
|
4898
4972
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
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;
|
|
4924
5013
|
}
|
|
4925
5014
|
}
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
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;
|
|
4934
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);
|
|
4935
5063
|
}
|
|
4936
5064
|
}
|
|
5065
|
+
clearSsrPropertyStore();
|
|
4937
5066
|
}
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
continue;
|
|
4949
|
-
const stateName = fragmentInfo.parseBindTextResult.stateName;
|
|
4950
|
-
const statePathName = fragmentInfo.parseBindTextResult.statePathName;
|
|
4951
|
-
const stateElement = getStateElementByName(rootNode, stateName);
|
|
4952
|
-
if (!stateElement)
|
|
4953
|
-
continue;
|
|
4954
|
-
stateElement.createState("readonly", (state) => {
|
|
4955
|
-
const list = state[statePathName];
|
|
4956
|
-
if (Array.isArray(list)) {
|
|
4957
|
-
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);
|
|
4958
5077
|
}
|
|
4959
|
-
}
|
|
5078
|
+
}
|
|
5079
|
+
for (const comment of toRemove) {
|
|
5080
|
+
comment.remove();
|
|
5081
|
+
}
|
|
4960
5082
|
}
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
const comment = walker.currentNode;
|
|
4976
|
-
if (comment.data === pattern) {
|
|
4977
|
-
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();
|
|
4978
5097
|
}
|
|
4979
5098
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
// elseif: 独自条件を持つが stateName は if から引き継ぐ
|
|
4995
|
-
if (bindingType === 'else' && lastIfParseResult) {
|
|
4996
|
-
parseBindTextResult = {
|
|
4997
|
-
...lastIfParseResult,
|
|
4998
|
-
outFilters: [...lastIfParseResult.outFilters, createNotFilter()],
|
|
4999
|
-
bindingType: 'else',
|
|
5000
|
-
};
|
|
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
|
+
}
|
|
5001
5113
|
}
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
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
|
+
}
|
|
5007
5129
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
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
|
+
}
|
|
5011
5154
|
}
|
|
5012
|
-
|
|
5013
|
-
|
|
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
|
+
}
|
|
5014
5163
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
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();
|
|
5017
5180
|
}
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
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();
|
|
5023
5220
|
}
|
|
5024
|
-
collectStructuralFragments(rootNode, fragment, forPath);
|
|
5025
|
-
const fragmentInfo = {
|
|
5026
|
-
fragment,
|
|
5027
|
-
parseBindTextResult,
|
|
5028
|
-
nodeInfos: getFragmentNodeInfos(fragment),
|
|
5029
|
-
};
|
|
5030
|
-
setFragmentInfoByUUID(uuid, rootNode, fragmentInfo);
|
|
5031
5221
|
}
|
|
5032
5222
|
}
|
|
5223
|
+
|
|
5224
|
+
// ハイドレーション時にスキップするバインディングタイプ
|
|
5225
|
+
const STRUCTURAL_TYPES = new Set(['for', 'if', 'elseif', 'else']);
|
|
5033
5226
|
/**
|
|
5034
|
-
* SSR
|
|
5035
|
-
* バージョン不一致時は DOM をクリーンアップして false を返す
|
|
5036
|
-
* (呼び出し元で buildBindings にフォールバック)。
|
|
5227
|
+
* SSR ブロック境界コメントを走査して、start〜end 間のノードを収集する
|
|
5037
5228
|
*/
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
const
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
console.warn(`[@wcstack/state] SSR version mismatch: server="${ssrEl.version}", client="${VERSION}". Falling back to full render.`);
|
|
5046
|
-
Ssr.cleanupDom(root);
|
|
5047
|
-
return false;
|
|
5048
|
-
}
|
|
5049
|
-
}
|
|
5050
|
-
// <wcs-ssr> からテンプレートを fragmentInfoByUUID に復帰
|
|
5051
|
-
for (const ssrNode of ssrElements) {
|
|
5052
|
-
restoreFragments(root, ssrNode);
|
|
5053
|
-
}
|
|
5054
|
-
// SSR ブロック境界コメントから既存 DOM を Content 化
|
|
5055
|
-
const blocks = collectSsrBlocks(document.body);
|
|
5056
|
-
hydrateBlocks(document.body, blocks);
|
|
5057
|
-
// ブロック境界コメント (start/end) を除去
|
|
5058
|
-
Ssr.removeBlockBoundaryComments(document.body);
|
|
5059
|
-
// <wcs-ssr> を一時除去(バインディング走査に含めない)
|
|
5060
|
-
const ssrParents = [];
|
|
5061
|
-
for (const el of ssrElements) {
|
|
5062
|
-
if (el.parentNode) {
|
|
5063
|
-
ssrParents.push({ el, parent: el.parentNode, next: el.nextSibling });
|
|
5064
|
-
el.remove();
|
|
5065
|
-
}
|
|
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);
|
|
5066
5236
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
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);
|
|
5082
5252
|
}
|
|
5083
5253
|
else {
|
|
5084
|
-
//
|
|
5085
|
-
|
|
5254
|
+
// uuid:path
|
|
5255
|
+
uuid = parts[0];
|
|
5256
|
+
path = parts.slice(1).join(':');
|
|
5086
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;
|
|
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);
|
|
5087
5288
|
}
|
|
5088
|
-
//
|
|
5089
|
-
const
|
|
5090
|
-
|
|
5289
|
+
// バインディング収集
|
|
5290
|
+
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(wrapper);
|
|
5291
|
+
// _initializeBindings 相当の処理
|
|
5091
5292
|
for (const binding of allBindings) {
|
|
5092
5293
|
replaceToReplaceNode(binding);
|
|
5093
|
-
if (attachEventHandler(binding))
|
|
5294
|
+
if (attachEventHandler(binding))
|
|
5094
5295
|
continue;
|
|
5095
|
-
|
|
5096
|
-
if (attachEventTokenHandler(binding)) {
|
|
5296
|
+
if (attachEventTokenHandler(binding))
|
|
5097
5297
|
continue;
|
|
5098
|
-
}
|
|
5099
5298
|
attachTwowayEventHandler(binding);
|
|
5100
5299
|
attachRadioEventHandler(binding);
|
|
5101
5300
|
attachCheckboxEventHandler(binding);
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
continue;
|
|
5108
|
-
}
|
|
5109
|
-
else {
|
|
5110
|
-
normalBindings.push(binding);
|
|
5301
|
+
}
|
|
5302
|
+
// 元の位置に戻す
|
|
5303
|
+
if (parent) {
|
|
5304
|
+
while (wrapper.firstChild) {
|
|
5305
|
+
parent.insertBefore(wrapper.firstChild, nextSibling);
|
|
5111
5306
|
}
|
|
5112
5307
|
}
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
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
|
+
}
|
|
5121
5331
|
}
|
|
5122
|
-
|
|
5123
|
-
|
|
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
|
+
}
|
|
5124
5339
|
}
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
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);
|
|
5140
5365
|
}
|
|
5141
5366
|
}
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
for (const ssrNode of restoredSsrElements) {
|
|
5152
|
-
const ssrEl = ssrNode;
|
|
5153
|
-
const props = ssrEl.hydrateProps;
|
|
5154
|
-
for (const [id, propMap] of Object.entries(props)) {
|
|
5155
|
-
const target = root.querySelector(`[data-wcs-ssr-id="${id}"]`);
|
|
5156
|
-
if (!target)
|
|
5157
|
-
continue;
|
|
5158
|
-
for (const [propName, value] of Object.entries(propMap)) {
|
|
5159
|
-
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
|
+
}
|
|
5160
5376
|
}
|
|
5161
5377
|
}
|
|
5162
5378
|
}
|
|
5163
|
-
//
|
|
5164
|
-
const
|
|
5165
|
-
|
|
5166
|
-
|
|
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
|
+
});
|
|
5167
5401
|
}
|
|
5168
|
-
return true;
|
|
5169
5402
|
}
|
|
5170
|
-
|
|
5171
|
-
const
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
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)
|
|
5176
5412
|
return null;
|
|
5177
|
-
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
function getBindingsReady(rootNode) {
|
|
5184
|
-
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
5185
|
-
}
|
|
5186
|
-
function setStateElementByName(rootNode, name, element) {
|
|
5187
|
-
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
5188
|
-
if (element === null) {
|
|
5189
|
-
// 削除の場合、Mapが存在しない場合は何もしない
|
|
5190
|
-
if (!stateElementByName) {
|
|
5191
|
-
return;
|
|
5192
|
-
}
|
|
5193
|
-
stateElementByName.delete(name);
|
|
5194
|
-
if (stateElementByName.size === 0) {
|
|
5195
|
-
stateElementByNameByNode.delete(rootNode);
|
|
5196
|
-
}
|
|
5197
|
-
if (config.debug) {
|
|
5198
|
-
console.debug(`State element unregistered: name="${name}"`);
|
|
5199
|
-
}
|
|
5200
|
-
}
|
|
5201
|
-
else {
|
|
5202
|
-
// 登録の場合
|
|
5203
|
-
if (!stateElementByName) {
|
|
5204
|
-
stateElementByName = new Map();
|
|
5205
|
-
stateElementByNameByNode.set(rootNode, stateElementByName);
|
|
5206
|
-
// 初めてルートノードに登録する場合
|
|
5207
|
-
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
5208
|
-
const enableSsr = !inSsr() && element.hasAttribute?.('enable-ssr');
|
|
5209
|
-
if (rootNode.constructor.name === 'HTMLDocument' || rootNode.constructor.name === 'Document') {
|
|
5210
|
-
const ready = new Promise((resolve) => {
|
|
5211
|
-
queueMicrotask(async () => {
|
|
5212
|
-
if (enableSsr) {
|
|
5213
|
-
const success = await hydrateBindings(rootNode);
|
|
5214
|
-
if (!success) {
|
|
5215
|
-
await buildBindings(rootNode);
|
|
5216
|
-
}
|
|
5217
|
-
}
|
|
5218
|
-
else {
|
|
5219
|
-
await buildBindings(rootNode);
|
|
5220
|
-
}
|
|
5221
|
-
resolve();
|
|
5222
|
-
});
|
|
5223
|
-
});
|
|
5224
|
-
bindingsReadyByNode.set(rootNode, ready);
|
|
5225
|
-
}
|
|
5226
|
-
else if (rootNode.constructor.name === 'ShadowRoot') {
|
|
5227
|
-
const ready = new Promise((resolve) => {
|
|
5228
|
-
queueMicrotask(async () => {
|
|
5229
|
-
await buildBindings(rootNode);
|
|
5230
|
-
resolve();
|
|
5231
|
-
});
|
|
5232
|
-
});
|
|
5233
|
-
bindingsReadyByNode.set(rootNode, ready);
|
|
5234
|
-
}
|
|
5235
|
-
}
|
|
5236
|
-
if (stateElementByName.has(name)) {
|
|
5237
|
-
raiseError(`State element with name "${name}" is already registered.`);
|
|
5238
|
-
}
|
|
5239
|
-
stateElementByName.set(name, element);
|
|
5240
|
-
if (config.debug) {
|
|
5241
|
-
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;
|
|
5242
5419
|
}
|
|
5243
5420
|
}
|
|
5421
|
+
return null;
|
|
5244
5422
|
}
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
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
|
+
};
|
|
5257
5442
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
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);
|
|
5267
5464
|
}
|
|
5465
|
+
collectStructuralFragments(rootNode, fragment, forPath);
|
|
5466
|
+
const fragmentInfo = {
|
|
5467
|
+
fragment,
|
|
5468
|
+
parseBindTextResult,
|
|
5469
|
+
nodeInfos: getFragmentNodeInfos(fragment),
|
|
5470
|
+
};
|
|
5471
|
+
setFragmentInfoByUUID(uuid, rootNode, fragmentInfo);
|
|
5268
5472
|
}
|
|
5269
5473
|
}
|
|
5270
|
-
function getFragmentInfoByUUID(uuid) {
|
|
5271
|
-
return fragmentInfoByUUID.get(uuid) || null;
|
|
5272
|
-
}
|
|
5273
|
-
function getAllFragmentUUIDs() {
|
|
5274
|
-
return Array.from(fragmentInfoByUUID.keys());
|
|
5275
|
-
}
|
|
5276
|
-
|
|
5277
|
-
// SSR コメントパターン
|
|
5278
|
-
const SSR_PLACEHOLDER_COMMENT = /^@@wcs-(?:for|if|elseif|else):[^-]/;
|
|
5279
|
-
const SSR_BLOCK_START = /^@@wcs-(for|if|elseif|else)-start:(.+)$/;
|
|
5280
|
-
const SSR_BLOCK_END = /^@@wcs-(for|if|elseif|else)-end:(.+)$/;
|
|
5281
|
-
const SSR_TEXT_START = /^@@wcs-text-start:(.+)$/;
|
|
5282
5474
|
/**
|
|
5283
|
-
*
|
|
5284
|
-
*
|
|
5285
|
-
*
|
|
5286
|
-
* "<" ">" "&" と U+2028/U+2029 を JSON の \uXXXX エスケープへ置換する
|
|
5287
|
-
* (JSON.parse では元の文字列と等価に復元される)。
|
|
5475
|
+
* SSR ハイドレーション用バインディング初期化。
|
|
5476
|
+
* バージョン不一致時は DOM をクリーンアップして false を返す
|
|
5477
|
+
* (呼び出し元で buildBindings にフォールバック)。
|
|
5288
5478
|
*/
|
|
5289
|
-
function
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
.
|
|
5296
|
-
}
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
_hydrateProps = null;
|
|
5301
|
-
get name() {
|
|
5302
|
-
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
|
+
}
|
|
5303
5490
|
}
|
|
5304
|
-
|
|
5305
|
-
|
|
5491
|
+
// <wcs-ssr> からテンプレートを fragmentInfoByUUID に復帰
|
|
5492
|
+
for (const ssrNode of ssrElements) {
|
|
5493
|
+
restoreFragments(root, ssrNode);
|
|
5306
5494
|
}
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
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();
|
|
5310
5506
|
}
|
|
5311
|
-
return this._stateData;
|
|
5312
5507
|
}
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
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
|
+
}
|
|
5316
5523
|
}
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
if (this._hydrateProps === null) {
|
|
5321
|
-
this._hydrateProps = this._loadHydrateProps();
|
|
5524
|
+
else {
|
|
5525
|
+
// コメントノード等
|
|
5526
|
+
setLoopContextByNode(node, null);
|
|
5322
5527
|
}
|
|
5323
|
-
return this._hydrateProps;
|
|
5324
|
-
}
|
|
5325
|
-
getTemplate(uuid) {
|
|
5326
|
-
return this.templates.get(uuid) ?? null;
|
|
5327
|
-
}
|
|
5328
|
-
/**
|
|
5329
|
-
* サーバーの SSR バージョンとクライアントの state バージョンを検証する。
|
|
5330
|
-
* メジャー・マイナーバージョンが一致すればtrue。
|
|
5331
|
-
* version 属性がない場合は検証スキップ(true)。
|
|
5332
|
-
*/
|
|
5333
|
-
verifyVersion() {
|
|
5334
|
-
const serverVersion = this.version;
|
|
5335
|
-
if (!serverVersion)
|
|
5336
|
-
return true;
|
|
5337
|
-
const serverParts = serverVersion.split('.');
|
|
5338
|
-
const clientParts = VERSION.split('.');
|
|
5339
|
-
// メジャー・マイナーが一致すれば互換
|
|
5340
|
-
return serverParts[0] === clientParts[0] && serverParts[1] === clientParts[1];
|
|
5341
|
-
}
|
|
5342
|
-
setStateData(data) {
|
|
5343
|
-
this._stateData = data;
|
|
5344
|
-
}
|
|
5345
|
-
setHydrateProps(props) {
|
|
5346
|
-
this._hydrateProps = props;
|
|
5347
5528
|
}
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5529
|
+
// バインディングを構造系とそれ以外に分離
|
|
5530
|
+
const normalBindings = [];
|
|
5531
|
+
const structuralBindings = [];
|
|
5532
|
+
for (const binding of allBindings) {
|
|
5533
|
+
replaceToReplaceNode(binding);
|
|
5534
|
+
if (attachEventHandler(binding)) {
|
|
5535
|
+
continue;
|
|
5354
5536
|
}
|
|
5355
|
-
|
|
5356
|
-
|
|
5537
|
+
if (attachEventTokenHandler(binding)) {
|
|
5538
|
+
continue;
|
|
5357
5539
|
}
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
const id = tpl.getAttribute('id');
|
|
5364
|
-
if (id) {
|
|
5365
|
-
map.set(id, tpl);
|
|
5366
|
-
}
|
|
5540
|
+
attachTwowayEventHandler(binding);
|
|
5541
|
+
attachRadioEventHandler(binding);
|
|
5542
|
+
attachCheckboxEventHandler(binding);
|
|
5543
|
+
if (STRUCTURAL_TYPES.has(binding.bindingType)) {
|
|
5544
|
+
structuralBindings.push(binding);
|
|
5367
5545
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
const script = this.querySelector('script[data-wcs-ssr-props]');
|
|
5372
|
-
if (!script)
|
|
5373
|
-
return {};
|
|
5374
|
-
try {
|
|
5375
|
-
return JSON.parse(script.textContent || '{}');
|
|
5546
|
+
else if (binding.statePathName.includes(WILDCARD)) {
|
|
5547
|
+
// for ブロック内のバインディング → Content のバインディングとして登録済み
|
|
5548
|
+
continue;
|
|
5376
5549
|
}
|
|
5377
|
-
|
|
5378
|
-
|
|
5550
|
+
else {
|
|
5551
|
+
normalBindings.push(binding);
|
|
5379
5552
|
}
|
|
5380
5553
|
}
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
const
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
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
|
+
}
|
|
5392
5566
|
}
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
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
|
+
});
|
|
5408
5581
|
}
|
|
5409
5582
|
}
|
|
5410
|
-
return data;
|
|
5411
5583
|
}
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
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)
|
|
5423
5598
|
continue;
|
|
5424
|
-
const
|
|
5425
|
-
|
|
5426
|
-
const bindResult = fragmentInfo.parseBindTextResult;
|
|
5427
|
-
const bindText = bindResult.bindingType === 'else'
|
|
5428
|
-
? 'else:'
|
|
5429
|
-
: `${bindResult.bindingType}: ${bindResult.statePathName}`;
|
|
5430
|
-
tpl.setAttribute(config.bindAttributeName, bindText);
|
|
5431
|
-
const content = fragmentInfo.fragment.cloneNode(true);
|
|
5432
|
-
tpl.content.appendChild(content);
|
|
5433
|
-
ssrEl.appendChild(tpl);
|
|
5434
|
-
}
|
|
5435
|
-
// 属性で代替不可なプロパティをハイドレーション用に格納
|
|
5436
|
-
const ssrNodes = getAllSsrPropertyNodes();
|
|
5437
|
-
if (ssrNodes.length > 0) {
|
|
5438
|
-
const propsData = {};
|
|
5439
|
-
for (let i = 0; i < ssrNodes.length; i++) {
|
|
5440
|
-
const node = ssrNodes[i];
|
|
5441
|
-
const entries = getSsrProperties(node);
|
|
5442
|
-
if (entries.length === 0)
|
|
5443
|
-
continue;
|
|
5444
|
-
const id = `wcs-ssr-${i}`;
|
|
5445
|
-
node.setAttribute('data-wcs-ssr-id', id);
|
|
5446
|
-
const props = {};
|
|
5447
|
-
for (const entry of entries) {
|
|
5448
|
-
props[entry.propName] = entry.value;
|
|
5449
|
-
}
|
|
5450
|
-
propsData[id] = props;
|
|
5451
|
-
}
|
|
5452
|
-
if (Object.keys(propsData).length > 0) {
|
|
5453
|
-
const propsScript = document.createElement('script');
|
|
5454
|
-
propsScript.setAttribute('type', 'application/json');
|
|
5455
|
-
propsScript.setAttribute('data-wcs-ssr-props', '');
|
|
5456
|
-
propsScript.textContent = escapeJsonForScript(JSON.stringify(propsData));
|
|
5457
|
-
ssrEl.appendChild(propsScript);
|
|
5599
|
+
for (const [propName, value] of Object.entries(propMap)) {
|
|
5600
|
+
target[propName] = value;
|
|
5458
5601
|
}
|
|
5459
5602
|
}
|
|
5460
|
-
clearSsrPropertyStore();
|
|
5461
5603
|
}
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
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 });
|
|
5472
5650
|
}
|
|
5473
5651
|
}
|
|
5474
|
-
|
|
5475
|
-
|
|
5652
|
+
if (config.debug) {
|
|
5653
|
+
console.debug(`State element unregistered: name="${name}"`);
|
|
5476
5654
|
}
|
|
5477
5655
|
}
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
const
|
|
5486
|
-
if (
|
|
5487
|
-
|
|
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);
|
|
5488
5689
|
}
|
|
5489
5690
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
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);
|
|
5492
5701
|
}
|
|
5493
5702
|
}
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
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;
|
|
5507
5759
|
}
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
let sibling = comment.nextSibling;
|
|
5513
|
-
comment.remove();
|
|
5514
|
-
const endPattern = `@@wcs-text-end:${path}`;
|
|
5515
|
-
while (sibling) {
|
|
5516
|
-
const next = sibling.nextSibling;
|
|
5517
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5518
|
-
sibling.parentNode.removeChild(sibling);
|
|
5519
|
-
break;
|
|
5760
|
+
for (const binding of bindings) {
|
|
5761
|
+
if (binding.replaceNode.isConnected === false) {
|
|
5762
|
+
// 切断されているバインディングは無視
|
|
5763
|
+
continue;
|
|
5520
5764
|
}
|
|
5521
|
-
|
|
5522
|
-
sibling = next;
|
|
5765
|
+
processBindings.push(binding);
|
|
5523
5766
|
}
|
|
5524
5767
|
}
|
|
5768
|
+
applyChangeFromBindings(processBindings);
|
|
5769
|
+
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
5770
|
+
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
5771
|
+
notifyUpdateBatchListeners(absoluteAddressSet);
|
|
5525
5772
|
}
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
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;
|
|
5549
5816
|
}
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
const comment = walker1.currentNode;
|
|
5555
|
-
if (SSR_BLOCK_START.test(comment.data)) {
|
|
5556
|
-
startComments.push(comment);
|
|
5817
|
+
const sourceId = source.id;
|
|
5818
|
+
source._setSink((event) => {
|
|
5819
|
+
for (const listener of listeners) {
|
|
5820
|
+
listener.onEvent?.(sourceId, event);
|
|
5557
5821
|
}
|
|
5558
|
-
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
const next = sibling.nextSibling;
|
|
5567
|
-
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
5568
|
-
sibling.remove();
|
|
5569
|
-
break;
|
|
5570
|
-
}
|
|
5571
|
-
sibling.remove();
|
|
5572
|
-
sibling = next;
|
|
5822
|
+
});
|
|
5823
|
+
};
|
|
5824
|
+
return {
|
|
5825
|
+
version: DEVTOOLS_PROTOCOL_VERSION,
|
|
5826
|
+
sources,
|
|
5827
|
+
register(source) {
|
|
5828
|
+
if (sources.has(source.id)) {
|
|
5829
|
+
return;
|
|
5573
5830
|
}
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
// プレースホルダーコメント (@@wcs-for:uuid 等) をテンプレートに差し替え
|
|
5579
|
-
const walker2 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
5580
|
-
const placeholders = [];
|
|
5581
|
-
while (walker2.nextNode()) {
|
|
5582
|
-
const comment = walker2.currentNode;
|
|
5583
|
-
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
5584
|
-
const uuid = comment.data.split(':')[1];
|
|
5585
|
-
placeholders.push({ comment, uuid });
|
|
5831
|
+
sources.set(source.id, source);
|
|
5832
|
+
applySink(source);
|
|
5833
|
+
for (const listener of listeners) {
|
|
5834
|
+
listener.onSourceRegistered?.(source);
|
|
5586
5835
|
}
|
|
5587
|
-
}
|
|
5588
|
-
|
|
5589
|
-
const
|
|
5590
|
-
if (
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
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;
|
|
5598
5858
|
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
restored.content.appendChild(document.importNode(child, true));
|
|
5602
|
-
}
|
|
5859
|
+
for (const source of sources.values()) {
|
|
5860
|
+
applySink(source);
|
|
5603
5861
|
}
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
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).`);
|
|
5615
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;
|
|
5616
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);
|
|
5617
6003
|
}
|
|
5618
6004
|
|
|
5619
6005
|
async function loadFromInnerScript(script, name) {
|
|
@@ -5781,7 +6167,7 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
5781
6167
|
}
|
|
5782
6168
|
let token = registry.get(name);
|
|
5783
6169
|
if (typeof token === "undefined") {
|
|
5784
|
-
token = new CommandToken(name);
|
|
6170
|
+
token = new CommandToken(name, stateElement.name);
|
|
5785
6171
|
registry.set(name, token);
|
|
5786
6172
|
}
|
|
5787
6173
|
return token;
|
|
@@ -6301,74 +6687,6 @@ function clearStreamNamespace(stateElement) {
|
|
|
6301
6687
|
errorNamespaceByStateElement.delete(stateElement);
|
|
6302
6688
|
}
|
|
6303
6689
|
|
|
6304
|
-
const updateBatchListeners = new Set();
|
|
6305
|
-
/**
|
|
6306
|
-
* drain 終了リスナーを登録する。
|
|
6307
|
-
*/
|
|
6308
|
-
function registerUpdateBatchListener(listener) {
|
|
6309
|
-
updateBatchListeners.add(listener);
|
|
6310
|
-
}
|
|
6311
|
-
/**
|
|
6312
|
-
* 全リスナーに drain のバッチを通知する。
|
|
6313
|
-
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
6314
|
-
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
6315
|
-
*/
|
|
6316
|
-
function notifyUpdateBatchListeners(batch) {
|
|
6317
|
-
for (const listener of updateBatchListeners) {
|
|
6318
|
-
listener(batch);
|
|
6319
|
-
}
|
|
6320
|
-
}
|
|
6321
|
-
class Updater {
|
|
6322
|
-
_queueAbsoluteAddresses = [];
|
|
6323
|
-
constructor() {
|
|
6324
|
-
}
|
|
6325
|
-
enqueueAbsoluteAddress(absoluteAddress) {
|
|
6326
|
-
const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
|
|
6327
|
-
this._queueAbsoluteAddresses.push(absoluteAddress);
|
|
6328
|
-
if (requireStartProcess) {
|
|
6329
|
-
queueMicrotask(() => {
|
|
6330
|
-
const absoluteAddresses = this._queueAbsoluteAddresses;
|
|
6331
|
-
this._queueAbsoluteAddresses = [];
|
|
6332
|
-
this._applyChange(absoluteAddresses);
|
|
6333
|
-
});
|
|
6334
|
-
}
|
|
6335
|
-
}
|
|
6336
|
-
// テスト用に公開
|
|
6337
|
-
testApplyChange(absoluteAddresses) {
|
|
6338
|
-
this._applyChange(absoluteAddresses);
|
|
6339
|
-
}
|
|
6340
|
-
_applyChange(absoluteAddresses) {
|
|
6341
|
-
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
6342
|
-
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
6343
|
-
// Set による重複排除が正しく機能する
|
|
6344
|
-
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6345
|
-
const processBindings = [];
|
|
6346
|
-
for (const absoluteAddress of absoluteAddressSet) {
|
|
6347
|
-
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
6348
|
-
// アドレス等)に空 Set を生成・蓄積しない
|
|
6349
|
-
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6350
|
-
if (bindings === undefined) {
|
|
6351
|
-
continue;
|
|
6352
|
-
}
|
|
6353
|
-
for (const binding of bindings) {
|
|
6354
|
-
if (binding.replaceNode.isConnected === false) {
|
|
6355
|
-
// 切断されているバインディングは無視
|
|
6356
|
-
continue;
|
|
6357
|
-
}
|
|
6358
|
-
processBindings.push(binding);
|
|
6359
|
-
}
|
|
6360
|
-
}
|
|
6361
|
-
applyChangeFromBindings(processBindings);
|
|
6362
|
-
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
6363
|
-
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
6364
|
-
notifyUpdateBatchListeners(absoluteAddressSet);
|
|
6365
|
-
}
|
|
6366
|
-
}
|
|
6367
|
-
const updater = new Updater();
|
|
6368
|
-
function getUpdater() {
|
|
6369
|
-
return updater;
|
|
6370
|
-
}
|
|
6371
|
-
|
|
6372
6690
|
/**
|
|
6373
6691
|
* stream/argsTrace.ts
|
|
6374
6692
|
*
|
|
@@ -7658,11 +7976,17 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
7658
7976
|
// primitive 値かつ Object.is 同値なら、set / enqueue / walkDependency / DOM 適用 /
|
|
7659
7977
|
// $updatedCallback / DCC イベントを丸ごとスキップ(標準的なリアクティブ no-op)。
|
|
7660
7978
|
// 参照型(object/array)は in-place mutation 取りこぼし防止のため素通し(ガードしない)。
|
|
7979
|
+
// devtools write イベント用: guard が既に取得した旧値のみ流用する
|
|
7980
|
+
// (参照型のために追加の get はしない — protocol §4.2)
|
|
7981
|
+
let devOldValue;
|
|
7982
|
+
let devHasOldValue = false;
|
|
7661
7983
|
if (config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
7662
7984
|
const oldValue = getByAddress(target, address, receiver, handler);
|
|
7663
7985
|
if (Object.is(oldValue, value)) {
|
|
7664
7986
|
return true;
|
|
7665
7987
|
}
|
|
7988
|
+
devOldValue = oldValue;
|
|
7989
|
+
devHasOldValue = true;
|
|
7666
7990
|
}
|
|
7667
7991
|
// --- end same-value guard ---
|
|
7668
7992
|
const isSwappable = stateElement.elementPaths.has(address.pathInfo.path);
|
|
@@ -7670,6 +7994,15 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
7670
7994
|
stateElement.getterPaths.has(address.pathInfo.path);
|
|
7671
7995
|
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
7672
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
|
+
}
|
|
7673
8006
|
try {
|
|
7674
8007
|
if (isSwappable) {
|
|
7675
8008
|
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler);
|
|
@@ -8044,15 +8377,16 @@ async function setLoopContextAsync(handler, loopContext, callback) {
|
|
|
8044
8377
|
* StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
|
|
8045
8378
|
*
|
|
8046
8379
|
* 主な役割:
|
|
8047
|
-
* - 文字列プロパティの場合、特殊プロパティ($1
|
|
8380
|
+
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
|
|
8381
|
+
* $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
|
|
8048
8382
|
* - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
|
|
8049
8383
|
* - getByRefで構造化パス・リストインデックスに対応した値を取得
|
|
8050
8384
|
* - シンボルプロパティの場合はhandler.callableApi経由でAPIを呼び出し
|
|
8051
8385
|
* - それ以外はReflect.getで通常のプロパティアクセスを実行
|
|
8052
8386
|
*
|
|
8053
8387
|
* 設計ポイント:
|
|
8054
|
-
* - $1〜$
|
|
8055
|
-
* - $
|
|
8388
|
+
* - $1〜$128(MAX_WILDCARD_DEPTH)は直近のStatePropertyRefのリストインデックス値を返す特殊プロパティ
|
|
8389
|
+
* - $getAll, $resolve 等はAPI関数を、$command / $streamStatus / $streamError は名前空間を返す
|
|
8056
8390
|
* - 通常のプロパティアクセスもバインディングや多重ループに対応
|
|
8057
8391
|
* - シンボルAPIやReflect.getで拡張性・互換性も確保
|
|
8058
8392
|
*/
|
|
@@ -9225,6 +9559,8 @@ function bootstrapState(config) {
|
|
|
9225
9559
|
setConfig(config);
|
|
9226
9560
|
}
|
|
9227
9561
|
registerComponents();
|
|
9562
|
+
// DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
|
|
9563
|
+
registerDevtoolsSource();
|
|
9228
9564
|
}
|
|
9229
9565
|
|
|
9230
9566
|
/**
|
|
@@ -9424,6 +9760,9 @@ function getWcsManifest() {
|
|
|
9424
9760
|
STATE_COMMAND_NAMESPACE_NAME,
|
|
9425
9761
|
STATE_EVENT_TOKENS_NAME,
|
|
9426
9762
|
STATE_ON_NAME,
|
|
9763
|
+
STATE_STREAMS_NAME,
|
|
9764
|
+
STATE_STREAM_STATUS_NAMESPACE_NAME,
|
|
9765
|
+
STATE_STREAM_ERROR_NAMESPACE_NAME,
|
|
9427
9766
|
],
|
|
9428
9767
|
};
|
|
9429
9768
|
}
|