@wcstack/state 1.5.2 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +60 -5
- package/README.md +60 -5
- package/dist/index.d.ts +100 -2
- package/dist/index.esm.js +1066 -98
- 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/package.json +72 -71
package/dist/index.esm.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
let _inSsrCache = null;
|
|
2
|
+
function inSsr() {
|
|
3
|
+
if (_inSsrCache !== null)
|
|
4
|
+
return _inSsrCache;
|
|
5
|
+
const html = document.querySelector('html');
|
|
6
|
+
_inSsrCache = html ? html.hasAttribute('data-wcs-server') : false;
|
|
7
|
+
return _inSsrCache;
|
|
8
|
+
}
|
|
1
9
|
const _config = {
|
|
2
10
|
bindAttributeName: 'data-wcs',
|
|
3
11
|
commentTextPrefix: 'wcs-text',
|
|
@@ -7,6 +15,7 @@ const _config = {
|
|
|
7
15
|
commentElsePrefix: 'wcs-else',
|
|
8
16
|
tagNames: {
|
|
9
17
|
state: 'wcs-state',
|
|
18
|
+
ssr: 'wcs-ssr',
|
|
10
19
|
},
|
|
11
20
|
locale: 'en',
|
|
12
21
|
debug: false,
|
|
@@ -14,6 +23,9 @@ const _config = {
|
|
|
14
23
|
};
|
|
15
24
|
// backward compatible export (read-only usage)
|
|
16
25
|
const config = _config;
|
|
26
|
+
function getConfig() {
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
17
29
|
function setConfig(partialConfig) {
|
|
18
30
|
if (partialConfig.tagNames) {
|
|
19
31
|
Object.assign(_config.tagNames, partialConfig.tagNames);
|
|
@@ -47,73 +59,16 @@ function setConfig(partialConfig) {
|
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const blob = new Blob([script.text + uniq_comment], { type: "application/javascript" });
|
|
56
|
-
const url = URL.createObjectURL(blob);
|
|
57
|
-
try {
|
|
58
|
-
scriptModule = await import(url);
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
// Clean up blob URL to prevent memory leak
|
|
62
|
-
URL.revokeObjectURL(url);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// Fallback: Base64 encoding method (for test environment)
|
|
67
|
-
// Convert script to Base64 and import via data: URL
|
|
68
|
-
const b64 = btoa(String.fromCodePoint(...new TextEncoder().encode(script.text + uniq_comment)));
|
|
69
|
-
scriptModule = await import(`data:application/javascript;base64,${b64}`);
|
|
70
|
-
}
|
|
71
|
-
return (scriptModule && typeof scriptModule.default === 'object') ? scriptModule.default : {};
|
|
72
|
-
}
|
|
62
|
+
var version$1 = "1.6.2";
|
|
63
|
+
var pkg = {
|
|
64
|
+
version: version$1};
|
|
65
|
+
|
|
66
|
+
const VERSION = pkg.version;
|
|
73
67
|
|
|
74
68
|
function raiseError(message) {
|
|
75
69
|
throw new Error(`[@wcstack/state] ${message}`);
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
async function loadFromJsonFile(url) {
|
|
79
|
-
try {
|
|
80
|
-
const response = await fetch(url);
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
raiseError(`Failed to fetch JSON file: ${response.statusText}`);
|
|
83
|
-
}
|
|
84
|
-
const data = await response.json();
|
|
85
|
-
return data;
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
console.error('Failed to load JSON file:', e);
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function loadFromScriptFile(url) {
|
|
94
|
-
try {
|
|
95
|
-
const module = await import(/* @vite-ignore */ url);
|
|
96
|
-
return module.default || {};
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
raiseError(`Failed to load script file: ${e}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function loadFromScriptJson(id) {
|
|
104
|
-
const script = document.getElementById(id);
|
|
105
|
-
if (script && script.type === 'application/json') {
|
|
106
|
-
try {
|
|
107
|
-
const data = JSON.parse(script.textContent || '{}');
|
|
108
|
-
return data;
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
raiseError('Failed to parse JSON from script element:' + e);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
72
|
const bindingPromiseByNode = new WeakMap();
|
|
118
73
|
let id$1 = 0;
|
|
119
74
|
function getInitializeBindingPromiseByNode(node) {
|
|
@@ -1410,34 +1365,6 @@ function parseBindTextForEmbeddedNode(bindText) {
|
|
|
1410
1365
|
};
|
|
1411
1366
|
}
|
|
1412
1367
|
|
|
1413
|
-
const fragmentInfoByUUID = new Map();
|
|
1414
|
-
function setFragmentInfoByUUID(uuid, rootNode, fragmentInfo) {
|
|
1415
|
-
if (fragmentInfo === null) {
|
|
1416
|
-
fragmentInfoByUUID.delete(uuid);
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
fragmentInfoByUUID.set(uuid, fragmentInfo);
|
|
1420
|
-
const bindingPartial = fragmentInfo.parseBindTextResult;
|
|
1421
|
-
const stateElement = getStateElementByName(rootNode, bindingPartial.stateName);
|
|
1422
|
-
if (stateElement === null) {
|
|
1423
|
-
raiseError(`State element with name "${bindingPartial.stateName}" not found for fragment info.`);
|
|
1424
|
-
}
|
|
1425
|
-
stateElement.setPathInfo(bindingPartial.statePathName, bindingPartial.bindingType);
|
|
1426
|
-
for (const nodeInfo of fragmentInfo.nodeInfos) {
|
|
1427
|
-
for (const nodeBindingPartial of nodeInfo.parseBindTextResults) {
|
|
1428
|
-
const nodeStateElement = getStateElementByName(rootNode, nodeBindingPartial.stateName);
|
|
1429
|
-
if (nodeStateElement === null) {
|
|
1430
|
-
raiseError(`State element with name "${nodeBindingPartial.stateName}" not found for fragment info node.`);
|
|
1431
|
-
}
|
|
1432
|
-
nodeStateElement.setPathInfo(nodeBindingPartial.statePathName, nodeBindingPartial.bindingType);
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
function getFragmentInfoByUUID(uuid) {
|
|
1438
|
-
return fragmentInfoByUUID.get(uuid) || null;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
1368
|
function getParseBindTextResults(node) {
|
|
1442
1369
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1443
1370
|
const element = node;
|
|
@@ -2650,6 +2577,21 @@ class Content {
|
|
|
2650
2577
|
this._mounted = false;
|
|
2651
2578
|
}
|
|
2652
2579
|
}
|
|
2580
|
+
/**
|
|
2581
|
+
* SSR ハイドレーション用: 既存の DOM ノード配列から Content を生成する。
|
|
2582
|
+
* テンプレートからの clone ではなく、SSR で描画済みのノードをそのまま使う。
|
|
2583
|
+
*/
|
|
2584
|
+
function createContentFromNodes(nodes) {
|
|
2585
|
+
const fragment = document.createDocumentFragment();
|
|
2586
|
+
// ノードを fragment に移動せず、参照だけ持つ Content を作る
|
|
2587
|
+
const content = new Content(fragment);
|
|
2588
|
+
// Content の内部状態を直接設定
|
|
2589
|
+
content._childNodeArray = nodes;
|
|
2590
|
+
content._firstNode = nodes.length > 0 ? nodes[0] : null;
|
|
2591
|
+
content._lastNode = nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
2592
|
+
content._mounted = true; // SSR で既にマウント済み
|
|
2593
|
+
return content;
|
|
2594
|
+
}
|
|
2653
2595
|
function createContent(bindingInfo) {
|
|
2654
2596
|
if (typeof bindingInfo.uuid === 'undefined' || bindingInfo.uuid === null) {
|
|
2655
2597
|
raiseError(`BindingInfo.uuid is null.`);
|
|
@@ -2678,6 +2620,13 @@ const lastNodeByNode = new WeakMap();
|
|
|
2678
2620
|
const contentByListIndexByNode = new WeakMap();
|
|
2679
2621
|
const pooledContentsByNode = new WeakMap();
|
|
2680
2622
|
const isOnlyNodeInParentContentByNode = new WeakMap();
|
|
2623
|
+
// SSR ハイドレーション用: Content を ListIndex に登録する
|
|
2624
|
+
function hydrateSetContent(node, index, content) {
|
|
2625
|
+
setContent(node, index, content);
|
|
2626
|
+
}
|
|
2627
|
+
function hydrateSetLastNode(node, lastNode) {
|
|
2628
|
+
lastNodeByNode.set(node, lastNode);
|
|
2629
|
+
}
|
|
2681
2630
|
function getPooledContents(bindingInfo) {
|
|
2682
2631
|
return pooledContentsByNode.get(bindingInfo.node) || [];
|
|
2683
2632
|
}
|
|
@@ -2779,6 +2728,8 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
2779
2728
|
fragment = document.createDocumentFragment();
|
|
2780
2729
|
setRootNodeByFragment(fragment, context.rootNode);
|
|
2781
2730
|
}
|
|
2731
|
+
const ssrMode = inSsr();
|
|
2732
|
+
const uuid = bindingInfo.uuid ?? '';
|
|
2782
2733
|
for (const index of diff.newIndexes) {
|
|
2783
2734
|
let content;
|
|
2784
2735
|
// add
|
|
@@ -2792,14 +2743,30 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
2792
2743
|
}
|
|
2793
2744
|
// コンテント活性化の前にDOMツリーに追加しておく必要がある
|
|
2794
2745
|
if (fragment !== null) {
|
|
2746
|
+
if (ssrMode) {
|
|
2747
|
+
fragment.appendChild(document.createComment(`@@wcs-for-start:${uuid}:${listPathInfo.path}:${index.index}`));
|
|
2748
|
+
}
|
|
2795
2749
|
content.appendTo(fragment);
|
|
2750
|
+
if (ssrMode) {
|
|
2751
|
+
fragment.appendChild(document.createComment(`@@wcs-for-end:${uuid}:${listPathInfo.path}:${index.index}`));
|
|
2752
|
+
}
|
|
2796
2753
|
}
|
|
2797
2754
|
else {
|
|
2798
2755
|
// Update lastNode for next iteration to ensure correct order
|
|
2799
2756
|
// Ensure content is in correct position (e.g. if previous siblings were deleted/moved)
|
|
2800
2757
|
if (lastNode.nextSibling !== content.firstNode) {
|
|
2758
|
+
if (ssrMode) {
|
|
2759
|
+
const startComment = document.createComment(`@@wcs-for-start:${uuid}:${listPathInfo.path}:${index.index}`);
|
|
2760
|
+
lastNode.parentNode.insertBefore(startComment, lastNode.nextSibling);
|
|
2761
|
+
lastNode = startComment;
|
|
2762
|
+
}
|
|
2801
2763
|
content.mountAfter(lastNode);
|
|
2802
2764
|
}
|
|
2765
|
+
if (ssrMode) {
|
|
2766
|
+
const endComment = document.createComment(`@@wcs-for-end:${uuid}:${listPathInfo.path}:${index.index}`);
|
|
2767
|
+
const afterNode = content.lastNode ?? lastNode;
|
|
2768
|
+
afterNode.parentNode.insertBefore(endComment, afterNode.nextSibling);
|
|
2769
|
+
}
|
|
2803
2770
|
}
|
|
2804
2771
|
// コンテントを活性化
|
|
2805
2772
|
activateContent(content, loopContext, context);
|
|
@@ -2852,6 +2819,9 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
2852
2819
|
else {
|
|
2853
2820
|
content = contents.values().next().value;
|
|
2854
2821
|
}
|
|
2822
|
+
const ssrMode = inSsr();
|
|
2823
|
+
const uuid = bindingInfo.uuid ?? '';
|
|
2824
|
+
const keyword = bindingInfo.bindingType; // if, elseif, else
|
|
2855
2825
|
try {
|
|
2856
2826
|
if (!newValue) {
|
|
2857
2827
|
if (config.debug) {
|
|
@@ -2864,7 +2834,17 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
2864
2834
|
if (config.debug) {
|
|
2865
2835
|
console.log(`mount if content : ${bindingInfoText(bindingInfo)}`);
|
|
2866
2836
|
}
|
|
2867
|
-
|
|
2837
|
+
if (ssrMode) {
|
|
2838
|
+
const startComment = document.createComment(`@@wcs-${keyword}-start:${uuid}:${bindingInfo.statePathName}`);
|
|
2839
|
+
bindingInfo.node.parentNode.insertBefore(startComment, bindingInfo.node.nextSibling);
|
|
2840
|
+
content.mountAfter(startComment);
|
|
2841
|
+
const endComment = document.createComment(`@@wcs-${keyword}-end:${uuid}:${bindingInfo.statePathName}`);
|
|
2842
|
+
const afterNode = content.lastNode ?? startComment;
|
|
2843
|
+
afterNode.parentNode.insertBefore(endComment, afterNode.nextSibling);
|
|
2844
|
+
}
|
|
2845
|
+
else {
|
|
2846
|
+
content.mountAfter(bindingInfo.node);
|
|
2847
|
+
}
|
|
2868
2848
|
const loopContext = getLoopContextByNode(bindingInfo.node);
|
|
2869
2849
|
activateContent(content, loopContext, context);
|
|
2870
2850
|
}
|
|
@@ -2874,6 +2854,82 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
2874
2854
|
}
|
|
2875
2855
|
}
|
|
2876
2856
|
|
|
2857
|
+
/**
|
|
2858
|
+
* SSR 時に HTML 属性で表現できないプロパティバインディングを蓄積するストア。
|
|
2859
|
+
* ハイドレーション時にクライアント側で復元する。
|
|
2860
|
+
*/
|
|
2861
|
+
// node → プロパティエントリのリスト
|
|
2862
|
+
const store = new WeakMap();
|
|
2863
|
+
function addSsrProperty(node, propName, value) {
|
|
2864
|
+
let entries = store.get(node);
|
|
2865
|
+
if (!entries) {
|
|
2866
|
+
entries = [];
|
|
2867
|
+
store.set(node, entries);
|
|
2868
|
+
}
|
|
2869
|
+
// 同じプロパティの既存エントリは上書き
|
|
2870
|
+
const existing = entries.find(e => e.propName === propName);
|
|
2871
|
+
if (existing) {
|
|
2872
|
+
existing.value = value;
|
|
2873
|
+
}
|
|
2874
|
+
else {
|
|
2875
|
+
entries.push({ propName, value });
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function getSsrProperties(node) {
|
|
2879
|
+
return store.get(node) ?? [];
|
|
2880
|
+
}
|
|
2881
|
+
function getAllSsrPropertyNodes() {
|
|
2882
|
+
// WeakMap は列挙不可なので、別途トラッキングが必要
|
|
2883
|
+
return Array.from(trackedNodes);
|
|
2884
|
+
}
|
|
2885
|
+
const trackedNodes = new Set();
|
|
2886
|
+
function trackSsrPropertyNode(node) {
|
|
2887
|
+
trackedNodes.add(node);
|
|
2888
|
+
}
|
|
2889
|
+
function clearSsrPropertyStore() {
|
|
2890
|
+
trackedNodes.clear();
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// SSR 時に HTML 属性で代替可能なプロパティ
|
|
2894
|
+
// これら以外のプロパティは ssrPropertyStore に蓄積してハイドレーション時に復元
|
|
2895
|
+
const SSR_ATTR_PROPS = {
|
|
2896
|
+
value(element, value) {
|
|
2897
|
+
if (element.tagName === 'TEXTAREA') {
|
|
2898
|
+
element.textContent = String(value ?? '');
|
|
2899
|
+
}
|
|
2900
|
+
else {
|
|
2901
|
+
element.setAttribute('value', String(value ?? ''));
|
|
2902
|
+
}
|
|
2903
|
+
},
|
|
2904
|
+
checked(element, value) {
|
|
2905
|
+
if (value)
|
|
2906
|
+
element.setAttribute('checked', '');
|
|
2907
|
+
else
|
|
2908
|
+
element.removeAttribute('checked');
|
|
2909
|
+
},
|
|
2910
|
+
selected(element, value) {
|
|
2911
|
+
if (value)
|
|
2912
|
+
element.setAttribute('selected', '');
|
|
2913
|
+
else
|
|
2914
|
+
element.removeAttribute('selected');
|
|
2915
|
+
},
|
|
2916
|
+
disabled(element, value) {
|
|
2917
|
+
if (value)
|
|
2918
|
+
element.setAttribute('disabled', '');
|
|
2919
|
+
else
|
|
2920
|
+
element.removeAttribute('disabled');
|
|
2921
|
+
},
|
|
2922
|
+
selectedIndex(element, value) {
|
|
2923
|
+
const options = element.querySelectorAll('option');
|
|
2924
|
+
const idx = Number(value);
|
|
2925
|
+
for (let i = 0; i < options.length; i++) {
|
|
2926
|
+
if (i === idx)
|
|
2927
|
+
options[i].setAttribute('selected', '');
|
|
2928
|
+
else
|
|
2929
|
+
options[i].removeAttribute('selected');
|
|
2930
|
+
}
|
|
2931
|
+
},
|
|
2932
|
+
};
|
|
2877
2933
|
function applyChangeToProperty(binding, _context, newValue) {
|
|
2878
2934
|
const element = binding.node;
|
|
2879
2935
|
const propSegments = binding.propSegments;
|
|
@@ -2893,6 +2949,18 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
2893
2949
|
}
|
|
2894
2950
|
}
|
|
2895
2951
|
}
|
|
2952
|
+
if (inSsr()) {
|
|
2953
|
+
const attrHandler = SSR_ATTR_PROPS[firstSegment];
|
|
2954
|
+
if (attrHandler) {
|
|
2955
|
+
// 属性で代替可能 → HTML 属性に反映
|
|
2956
|
+
attrHandler(element, newValue);
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
// 属性で代替不可 → ハイドレーション用ストアに蓄積
|
|
2960
|
+
addSsrProperty(element, firstSegment, newValue);
|
|
2961
|
+
trackSsrPropertyNode(element);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2896
2964
|
return;
|
|
2897
2965
|
}
|
|
2898
2966
|
const firstSegment = propSegments[0];
|
|
@@ -2932,6 +3000,7 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
2932
3000
|
}
|
|
2933
3001
|
}
|
|
2934
3002
|
}
|
|
3003
|
+
// サブオブジェクトプロパティ (e.g. style.xxx) は属性に反映済みなのでストア不要
|
|
2935
3004
|
}
|
|
2936
3005
|
|
|
2937
3006
|
function applyChangeToRadio(binding, _context, newValue) {
|
|
@@ -2950,10 +3019,23 @@ function applyChangeToStyle(binding, _context, newValue) {
|
|
|
2950
3019
|
}
|
|
2951
3020
|
}
|
|
2952
3021
|
|
|
3022
|
+
const ssrWrappedNodes = new WeakSet();
|
|
2953
3023
|
function applyChangeToText(binding, _context, newValue) {
|
|
2954
3024
|
if (binding.replaceNode.nodeValue !== newValue) {
|
|
2955
3025
|
binding.replaceNode.nodeValue = newValue;
|
|
2956
3026
|
}
|
|
3027
|
+
// SSR モード時: テキストノードの前後にコメントを挿入して境界を明示
|
|
3028
|
+
if (inSsr() && !ssrWrappedNodes.has(binding.replaceNode)) {
|
|
3029
|
+
ssrWrappedNodes.add(binding.replaceNode);
|
|
3030
|
+
const parentNode = binding.replaceNode.parentNode;
|
|
3031
|
+
if (parentNode) {
|
|
3032
|
+
const path = binding.statePathName;
|
|
3033
|
+
const startComment = document.createComment(`@@wcs-text-start:${path}`);
|
|
3034
|
+
const endComment = document.createComment(`@@wcs-text-end:${path}`);
|
|
3035
|
+
parentNode.insertBefore(startComment, binding.replaceNode);
|
|
3036
|
+
parentNode.insertBefore(endComment, binding.replaceNode.nextSibling);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
2957
3039
|
}
|
|
2958
3040
|
|
|
2959
3041
|
function applyChangeToWebComponent(binding, _context, newValue) {
|
|
@@ -3806,7 +3888,391 @@ async function buildBindings(root) {
|
|
|
3806
3888
|
}
|
|
3807
3889
|
}
|
|
3808
3890
|
|
|
3891
|
+
// ハイドレーション時にスキップするバインディングタイプ
|
|
3892
|
+
const STRUCTURAL_TYPES = new Set(['for', 'if', 'elseif', 'else']);
|
|
3893
|
+
/**
|
|
3894
|
+
* SSR ブロック境界コメントを走査して、start〜end 間のノードを収集する
|
|
3895
|
+
*/
|
|
3896
|
+
function collectSsrBlocks(root) {
|
|
3897
|
+
const blocks = [];
|
|
3898
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
3899
|
+
const startComments = [];
|
|
3900
|
+
// まず全コメントを収集
|
|
3901
|
+
while (walker.nextNode()) {
|
|
3902
|
+
startComments.push(walker.currentNode);
|
|
3903
|
+
}
|
|
3904
|
+
for (const comment of startComments) {
|
|
3905
|
+
const startMatch = SSR_BLOCK_START.exec(comment.data);
|
|
3906
|
+
if (!startMatch)
|
|
3907
|
+
continue;
|
|
3908
|
+
const type = startMatch[1];
|
|
3909
|
+
const info = startMatch[2]; // "uuid:path:index" or "uuid:path"
|
|
3910
|
+
const parts = info.split(':');
|
|
3911
|
+
let uuid;
|
|
3912
|
+
let path;
|
|
3913
|
+
let index = null;
|
|
3914
|
+
if (type === 'for') {
|
|
3915
|
+
// uuid:path:index
|
|
3916
|
+
uuid = parts[0];
|
|
3917
|
+
path = parts[1];
|
|
3918
|
+
index = parseInt(parts[2], 10);
|
|
3919
|
+
}
|
|
3920
|
+
else {
|
|
3921
|
+
// uuid:path
|
|
3922
|
+
uuid = parts[0];
|
|
3923
|
+
path = parts.slice(1).join(':');
|
|
3924
|
+
}
|
|
3925
|
+
// start と end の間のノードを収集
|
|
3926
|
+
const nodes = [];
|
|
3927
|
+
let sibling = comment.nextSibling;
|
|
3928
|
+
const endPattern = `@@wcs-${type}-end:${info}`;
|
|
3929
|
+
while (sibling) {
|
|
3930
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
3931
|
+
break;
|
|
3932
|
+
}
|
|
3933
|
+
nodes.push(sibling);
|
|
3934
|
+
sibling = sibling.nextSibling;
|
|
3935
|
+
}
|
|
3936
|
+
blocks.push({ type, uuid, path, index, nodes });
|
|
3937
|
+
}
|
|
3938
|
+
return blocks;
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* live DOM ノード群からバインディングを収集する。
|
|
3942
|
+
* ノードを一時的に DocumentFragment に移動して collectNodesAndBindingInfos を実行し、
|
|
3943
|
+
* 元の位置に戻す。
|
|
3944
|
+
*/
|
|
3945
|
+
function collectBindingsFromLiveNodes(nodes) {
|
|
3946
|
+
if (nodes.length === 0)
|
|
3947
|
+
return { bindingInfos: [], subscriberNodes: [] };
|
|
3948
|
+
// ノードの元の位置を記録
|
|
3949
|
+
const parent = nodes[0].parentNode;
|
|
3950
|
+
const nextSibling = nodes[nodes.length - 1].nextSibling;
|
|
3951
|
+
// 一時的に wrapper 要素に移動(collectNodesAndBindingInfos は Element を受け付ける)
|
|
3952
|
+
const wrapper = document.createElement('div');
|
|
3953
|
+
for (const node of nodes) {
|
|
3954
|
+
wrapper.appendChild(node);
|
|
3955
|
+
}
|
|
3956
|
+
// バインディング収集
|
|
3957
|
+
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(wrapper);
|
|
3958
|
+
// _initializeBindings 相当の処理
|
|
3959
|
+
for (const binding of allBindings) {
|
|
3960
|
+
replaceToReplaceNode(binding);
|
|
3961
|
+
if (attachEventHandler(binding))
|
|
3962
|
+
continue;
|
|
3963
|
+
attachTwowayEventHandler(binding);
|
|
3964
|
+
attachRadioEventHandler(binding);
|
|
3965
|
+
attachCheckboxEventHandler(binding);
|
|
3966
|
+
}
|
|
3967
|
+
// 元の位置に戻す
|
|
3968
|
+
if (parent) {
|
|
3969
|
+
while (wrapper.firstChild) {
|
|
3970
|
+
parent.insertBefore(wrapper.firstChild, nextSibling);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
return {
|
|
3974
|
+
bindingInfos: allBindings,
|
|
3975
|
+
subscriberNodes,
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
/**
|
|
3979
|
+
* SSR ブロックの DOM ノードを Content 化し、バインディングを登録する。
|
|
3980
|
+
*/
|
|
3981
|
+
function hydrateBlocks(root, blocks) {
|
|
3982
|
+
// for ブロックの listIndex を UUID ごとに収集
|
|
3983
|
+
const listIndexesByUuid = new Map();
|
|
3984
|
+
for (const block of blocks) {
|
|
3985
|
+
if (block.nodes.length === 0)
|
|
3986
|
+
continue;
|
|
3987
|
+
const content = createContentFromNodes(block.nodes);
|
|
3988
|
+
// Content のバインディングを収集
|
|
3989
|
+
const { bindingInfos, subscriberNodes } = collectBindingsFromLiveNodes(block.nodes);
|
|
3990
|
+
// Content 内のノードに data-wcs-completed を付与
|
|
3991
|
+
// (メインの collectNodesAndBindingInfos で重複登録されないようにする)
|
|
3992
|
+
for (const node of subscriberNodes) {
|
|
3993
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
3994
|
+
node.setAttribute('data-wcs-completed', '');
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
setBindingsByContent(content, bindingInfos);
|
|
3998
|
+
setNodesByContent(content, subscriberNodes);
|
|
3999
|
+
const indexBindings = [];
|
|
4000
|
+
for (const binding of bindingInfos) {
|
|
4001
|
+
if (binding.statePathName in INDEX_BY_INDEX_NAME) {
|
|
4002
|
+
indexBindings.push(binding);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
setIndexBindingsByContent(content, indexBindings);
|
|
4006
|
+
if (block.type === 'for' && block.index !== null) {
|
|
4007
|
+
const placeholderComment = findPlaceholderComment(root, 'for', block.uuid);
|
|
4008
|
+
if (placeholderComment) {
|
|
4009
|
+
const listIndex = createListIndex(null, block.index);
|
|
4010
|
+
hydrateSetContent(placeholderComment, listIndex, content);
|
|
4011
|
+
const lastNode = block.nodes[block.nodes.length - 1];
|
|
4012
|
+
hydrateSetLastNode(placeholderComment, lastNode);
|
|
4013
|
+
setContentByNode(placeholderComment, content);
|
|
4014
|
+
// ループコンテキストをバインドし、バインディングをアドレスに登録
|
|
4015
|
+
const pathInfo = getPathInfo(block.path + '.' + WILDCARD);
|
|
4016
|
+
const stateAddress = createStateAddress(pathInfo, listIndex);
|
|
4017
|
+
// ILoopContext は IStateAddress + listIndex なので、stateAddress をそのまま使う
|
|
4018
|
+
bindLoopContextToContent(content, stateAddress);
|
|
4019
|
+
for (const binding of bindingInfos) {
|
|
4020
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
4021
|
+
addBindingByAbsoluteStateAddress(absAddr, binding);
|
|
4022
|
+
}
|
|
4023
|
+
// listIndex を UUID ごとに収集(後で setListIndexesByList に渡す)
|
|
4024
|
+
let indexes = listIndexesByUuid.get(block.uuid);
|
|
4025
|
+
if (!indexes) {
|
|
4026
|
+
indexes = [];
|
|
4027
|
+
listIndexesByUuid.set(block.uuid, indexes);
|
|
4028
|
+
}
|
|
4029
|
+
indexes.push(listIndex);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
else {
|
|
4033
|
+
const placeholderComment = findPlaceholderComment(root, block.type, block.uuid);
|
|
4034
|
+
if (placeholderComment) {
|
|
4035
|
+
setContentByNode(placeholderComment, content);
|
|
4036
|
+
// バインディングをアドレスに登録
|
|
4037
|
+
for (const binding of bindingInfos) {
|
|
4038
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
4039
|
+
addBindingByAbsoluteStateAddress(absAddr, binding);
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
// for ブロックの listIndex を state のリスト値に紐づける
|
|
4045
|
+
for (const [uuid, indexes] of listIndexesByUuid) {
|
|
4046
|
+
const placeholderComment = findPlaceholderComment(root, 'for', uuid);
|
|
4047
|
+
if (!placeholderComment)
|
|
4048
|
+
continue;
|
|
4049
|
+
// state から現在のリスト値を取得して listIndexes を設定
|
|
4050
|
+
const rootNode = placeholderComment.getRootNode();
|
|
4051
|
+
// structuralBindings はまだ登録前なので、getParseBindTextResults を直接使う
|
|
4052
|
+
const fragmentInfo = getFragmentInfoByUUID(uuid);
|
|
4053
|
+
if (!fragmentInfo)
|
|
4054
|
+
continue;
|
|
4055
|
+
const stateName = fragmentInfo.parseBindTextResult.stateName;
|
|
4056
|
+
const statePathName = fragmentInfo.parseBindTextResult.statePathName;
|
|
4057
|
+
const stateElement = getStateElementByName(rootNode, stateName);
|
|
4058
|
+
if (!stateElement)
|
|
4059
|
+
continue;
|
|
4060
|
+
stateElement.createState("readonly", (state) => {
|
|
4061
|
+
const list = state[statePathName];
|
|
4062
|
+
if (Array.isArray(list)) {
|
|
4063
|
+
setListIndexesByList(list, indexes);
|
|
4064
|
+
}
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
function findPlaceholderComment(root, type, uuid) {
|
|
4069
|
+
const keywordMap = {
|
|
4070
|
+
'for': config.commentForPrefix,
|
|
4071
|
+
'if': config.commentIfPrefix,
|
|
4072
|
+
'elseif': config.commentElseIfPrefix,
|
|
4073
|
+
'else': config.commentElsePrefix,
|
|
4074
|
+
};
|
|
4075
|
+
const keyword = keywordMap[type];
|
|
4076
|
+
if (!keyword)
|
|
4077
|
+
return null;
|
|
4078
|
+
const pattern = `@@${keyword}:${uuid}`;
|
|
4079
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
4080
|
+
while (walker.nextNode()) {
|
|
4081
|
+
const comment = walker.currentNode;
|
|
4082
|
+
if (comment.data === pattern) {
|
|
4083
|
+
return comment;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
return null;
|
|
4087
|
+
}
|
|
4088
|
+
/**
|
|
4089
|
+
* <wcs-ssr> 内のテンプレートを fragmentInfoByUUID に復帰させる。
|
|
4090
|
+
*/
|
|
4091
|
+
function restoreFragments(root, ssrEl) {
|
|
4092
|
+
const rootNode = root;
|
|
4093
|
+
let lastIfParseResult = null;
|
|
4094
|
+
for (const [uuid, tpl] of ssrEl.templates) {
|
|
4095
|
+
const bindText = tpl.getAttribute(config.bindAttributeName) || '';
|
|
4096
|
+
const parseBindTextResults = parseBindTextsForElement(bindText);
|
|
4097
|
+
let parseBindTextResult = parseBindTextResults[0];
|
|
4098
|
+
const bindingType = parseBindTextResult.bindingType;
|
|
4099
|
+
// else: 直前の if 条件の not → 条件反転
|
|
4100
|
+
// elseif: 独自条件を持つが stateName は if から引き継ぐ
|
|
4101
|
+
if (bindingType === 'else' && lastIfParseResult) {
|
|
4102
|
+
parseBindTextResult = {
|
|
4103
|
+
...lastIfParseResult,
|
|
4104
|
+
outFilters: [...lastIfParseResult.outFilters, createNotFilter()],
|
|
4105
|
+
bindingType: 'else',
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
else if (bindingType === 'elseif' && lastIfParseResult) {
|
|
4109
|
+
parseBindTextResult = {
|
|
4110
|
+
...parseBindTextResult,
|
|
4111
|
+
stateName: lastIfParseResult.stateName,
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4114
|
+
// if chain の追跡
|
|
4115
|
+
if (bindingType === 'if') {
|
|
4116
|
+
lastIfParseResult = parseBindTextResult;
|
|
4117
|
+
}
|
|
4118
|
+
else if (bindingType === 'elseif') {
|
|
4119
|
+
lastIfParseResult = parseBindTextResult;
|
|
4120
|
+
}
|
|
4121
|
+
else if (bindingType === 'else') {
|
|
4122
|
+
lastIfParseResult = null;
|
|
4123
|
+
}
|
|
4124
|
+
const fragment = document.importNode(tpl.content, true);
|
|
4125
|
+
const forPath = bindingType === "for" ? parseBindTextResult.statePathName : undefined;
|
|
4126
|
+
optimizeFragment(fragment);
|
|
4127
|
+
if (typeof forPath === "string") {
|
|
4128
|
+
expandShorthandPaths(fragment, forPath);
|
|
4129
|
+
}
|
|
4130
|
+
collectStructuralFragments(rootNode, fragment, forPath);
|
|
4131
|
+
const fragmentInfo = {
|
|
4132
|
+
fragment,
|
|
4133
|
+
parseBindTextResult,
|
|
4134
|
+
nodeInfos: getFragmentNodeInfos(fragment),
|
|
4135
|
+
};
|
|
4136
|
+
setFragmentInfoByUUID(uuid, rootNode, fragmentInfo);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* SSR ハイドレーション用バインディング初期化。
|
|
4141
|
+
* バージョン不一致時は DOM をクリーンアップして false を返す
|
|
4142
|
+
* (呼び出し元で buildBindings にフォールバック)。
|
|
4143
|
+
*/
|
|
4144
|
+
async function hydrateBindings(root) {
|
|
4145
|
+
await waitForStateInitialize(root);
|
|
4146
|
+
// バージョン検証
|
|
4147
|
+
const ssrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
4148
|
+
for (const ssrNode of ssrElements) {
|
|
4149
|
+
const ssrEl = ssrNode;
|
|
4150
|
+
if (!ssrEl.verifyVersion()) {
|
|
4151
|
+
console.warn(`[@wcstack/state] SSR version mismatch: server="${ssrEl.version}", client="${VERSION}". Falling back to full render.`);
|
|
4152
|
+
Ssr.cleanupDom(root);
|
|
4153
|
+
return false;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
// <wcs-ssr> からテンプレートを fragmentInfoByUUID に復帰
|
|
4157
|
+
for (const ssrNode of ssrElements) {
|
|
4158
|
+
restoreFragments(root, ssrNode);
|
|
4159
|
+
}
|
|
4160
|
+
// SSR ブロック境界コメントから既存 DOM を Content 化
|
|
4161
|
+
const blocks = collectSsrBlocks(document.body);
|
|
4162
|
+
hydrateBlocks(document.body, blocks);
|
|
4163
|
+
// ブロック境界コメント (start/end) を除去
|
|
4164
|
+
Ssr.removeBlockBoundaryComments(document.body);
|
|
4165
|
+
// <wcs-ssr> を一時除去(バインディング走査に含めない)
|
|
4166
|
+
const ssrParents = [];
|
|
4167
|
+
for (const el of ssrElements) {
|
|
4168
|
+
if (el.parentNode) {
|
|
4169
|
+
ssrParents.push({ el, parent: el.parentNode, next: el.nextSibling });
|
|
4170
|
+
el.remove();
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
// 構造プレースホルダーコメント (@@wcs-for:uuid 等) は残す
|
|
4174
|
+
// → バインディング走査で拾われ、状態変化時の再レンダリングに使われる
|
|
4175
|
+
// SSR テキストバインディングを @@: 形式に復元
|
|
4176
|
+
Ssr.restoreTextBindings(document.body);
|
|
4177
|
+
// ノードとバインディングを収集
|
|
4178
|
+
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(document.body);
|
|
4179
|
+
// 収集完了したノードに data-wcs-completed 属性を付与
|
|
4180
|
+
// for ブロック内ノード(hydrateBlocks で登録済み)にはループコンテキストをリセットしない
|
|
4181
|
+
for (const node of subscriberNodes) {
|
|
4182
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4183
|
+
const el = node;
|
|
4184
|
+
if (!el.hasAttribute('data-wcs-completed')) {
|
|
4185
|
+
setLoopContextByNode(node, null);
|
|
4186
|
+
el.setAttribute('data-wcs-completed', '');
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
else {
|
|
4190
|
+
// コメントノード等
|
|
4191
|
+
setLoopContextByNode(node, null);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
// バインディングを構造系とそれ以外に分離
|
|
4195
|
+
const normalBindings = [];
|
|
4196
|
+
const structuralBindings = [];
|
|
4197
|
+
for (const binding of allBindings) {
|
|
4198
|
+
replaceToReplaceNode(binding);
|
|
4199
|
+
if (attachEventHandler(binding)) {
|
|
4200
|
+
continue;
|
|
4201
|
+
}
|
|
4202
|
+
attachTwowayEventHandler(binding);
|
|
4203
|
+
attachRadioEventHandler(binding);
|
|
4204
|
+
attachCheckboxEventHandler(binding);
|
|
4205
|
+
if (STRUCTURAL_TYPES.has(binding.bindingType)) {
|
|
4206
|
+
structuralBindings.push(binding);
|
|
4207
|
+
}
|
|
4208
|
+
else if (binding.statePathName.includes(WILDCARD)) {
|
|
4209
|
+
// for ブロック内のバインディング → Content のバインディングとして登録済み
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
else {
|
|
4213
|
+
normalBindings.push(binding);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
// 全バインディング(通常 + 構造)をアドレスに登録
|
|
4217
|
+
for (const binding of [...normalBindings, ...structuralBindings]) {
|
|
4218
|
+
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
4219
|
+
addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
4220
|
+
const rootNode = binding.replaceNode.getRootNode();
|
|
4221
|
+
const stateElement = getStateElementByName(rootNode, binding.stateName);
|
|
4222
|
+
if (stateElement === null) {
|
|
4223
|
+
raiseError(`State element with name "${binding.stateName}" not found for binding.`);
|
|
4224
|
+
}
|
|
4225
|
+
if (binding.bindingType !== 'event') {
|
|
4226
|
+
stateElement.setPathInfo(binding.statePathName, binding.bindingType);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
// for バインディングの lastListValue を初期値として設定
|
|
4230
|
+
// (次回の状態変化時に差分計算の基準になる)
|
|
4231
|
+
for (const binding of structuralBindings) {
|
|
4232
|
+
if (binding.bindingType === 'for') {
|
|
4233
|
+
const absAddr = getAbsoluteStateAddressByBinding(binding);
|
|
4234
|
+
const rootNode = binding.replaceNode.getRootNode();
|
|
4235
|
+
const stateElement = getStateElementByName(rootNode, binding.stateName);
|
|
4236
|
+
if (stateElement) {
|
|
4237
|
+
stateElement.createState("readonly", (state) => {
|
|
4238
|
+
const value = state[binding.statePathName];
|
|
4239
|
+
if (Array.isArray(value)) {
|
|
4240
|
+
setLastListValueByAbsoluteStateAddress(absAddr, value);
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
// 通常バインディングのみ初回値適用(構造バインディングはSSR描画済み)
|
|
4247
|
+
applyChangeFromBindings(normalBindings);
|
|
4248
|
+
// <wcs-ssr> を元に戻す
|
|
4249
|
+
for (const { el, parent, next } of ssrParents) {
|
|
4250
|
+
parent.insertBefore(el, next);
|
|
4251
|
+
}
|
|
4252
|
+
// hydrateProps 復元
|
|
4253
|
+
const restoredSsrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
4254
|
+
for (const ssrNode of restoredSsrElements) {
|
|
4255
|
+
const ssrEl = ssrNode;
|
|
4256
|
+
const props = ssrEl.hydrateProps;
|
|
4257
|
+
for (const [id, propMap] of Object.entries(props)) {
|
|
4258
|
+
const target = root.querySelector(`[data-wcs-ssr-id="${id}"]`);
|
|
4259
|
+
if (!target)
|
|
4260
|
+
continue;
|
|
4261
|
+
for (const [propName, value] of Object.entries(propMap)) {
|
|
4262
|
+
target[propName] = value;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
// ハイドレーション中の重複登録防止用属性を除去
|
|
4267
|
+
const completedEls = root.querySelectorAll('[data-wcs-completed]');
|
|
4268
|
+
for (const el of completedEls) {
|
|
4269
|
+
el.removeAttribute('data-wcs-completed');
|
|
4270
|
+
}
|
|
4271
|
+
return true;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
3809
4274
|
const stateElementByNameByNode = new WeakMap();
|
|
4275
|
+
const bindingsReadyByNode = new WeakMap();
|
|
3810
4276
|
function getStateElementByName(rootNode, name) {
|
|
3811
4277
|
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
3812
4278
|
if (!stateElementByName) {
|
|
@@ -3814,6 +4280,12 @@ function getStateElementByName(rootNode, name) {
|
|
|
3814
4280
|
}
|
|
3815
4281
|
return stateElementByName.get(name) || null;
|
|
3816
4282
|
}
|
|
4283
|
+
/**
|
|
4284
|
+
* 指定された rootNode のバインディング初期化が完了するまで待機する Promise を返す。
|
|
4285
|
+
*/
|
|
4286
|
+
function getBindingsReady(rootNode) {
|
|
4287
|
+
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
4288
|
+
}
|
|
3817
4289
|
function setStateElementByName(rootNode, name, element) {
|
|
3818
4290
|
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
3819
4291
|
if (element === null) {
|
|
@@ -3835,15 +4307,33 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
3835
4307
|
stateElementByName = new Map();
|
|
3836
4308
|
stateElementByNameByNode.set(rootNode, stateElementByName);
|
|
3837
4309
|
// 初めてルートノードに登録する場合
|
|
4310
|
+
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
4311
|
+
const enableSsr = !inSsr() && element.hasAttribute?.('enable-ssr');
|
|
3838
4312
|
if (rootNode.constructor.name === 'HTMLDocument' || rootNode.constructor.name === 'Document') {
|
|
3839
|
-
|
|
3840
|
-
|
|
4313
|
+
const ready = new Promise((resolve) => {
|
|
4314
|
+
queueMicrotask(async () => {
|
|
4315
|
+
if (enableSsr) {
|
|
4316
|
+
const success = await hydrateBindings(rootNode);
|
|
4317
|
+
if (!success) {
|
|
4318
|
+
await buildBindings(rootNode);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
else {
|
|
4322
|
+
await buildBindings(rootNode);
|
|
4323
|
+
}
|
|
4324
|
+
resolve();
|
|
4325
|
+
});
|
|
3841
4326
|
});
|
|
4327
|
+
bindingsReadyByNode.set(rootNode, ready);
|
|
3842
4328
|
}
|
|
3843
4329
|
else if (rootNode.constructor.name === 'ShadowRoot') {
|
|
3844
|
-
|
|
3845
|
-
|
|
4330
|
+
const ready = new Promise((resolve) => {
|
|
4331
|
+
queueMicrotask(async () => {
|
|
4332
|
+
await buildBindings(rootNode);
|
|
4333
|
+
resolve();
|
|
4334
|
+
});
|
|
3846
4335
|
});
|
|
4336
|
+
bindingsReadyByNode.set(rootNode, ready);
|
|
3847
4337
|
}
|
|
3848
4338
|
}
|
|
3849
4339
|
if (stateElementByName.has(name)) {
|
|
@@ -3856,6 +4346,427 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
3856
4346
|
}
|
|
3857
4347
|
}
|
|
3858
4348
|
|
|
4349
|
+
const fragmentInfoByUUID = new Map();
|
|
4350
|
+
function setFragmentInfoByUUID(uuid, rootNode, fragmentInfo) {
|
|
4351
|
+
if (fragmentInfo === null) {
|
|
4352
|
+
fragmentInfoByUUID.delete(uuid);
|
|
4353
|
+
}
|
|
4354
|
+
else {
|
|
4355
|
+
fragmentInfoByUUID.set(uuid, fragmentInfo);
|
|
4356
|
+
const bindingPartial = fragmentInfo.parseBindTextResult;
|
|
4357
|
+
const stateElement = getStateElementByName(rootNode, bindingPartial.stateName);
|
|
4358
|
+
if (stateElement === null) {
|
|
4359
|
+
raiseError(`State element with name "${bindingPartial.stateName}" not found for fragment info.`);
|
|
4360
|
+
}
|
|
4361
|
+
stateElement.setPathInfo(bindingPartial.statePathName, bindingPartial.bindingType);
|
|
4362
|
+
for (const nodeInfo of fragmentInfo.nodeInfos) {
|
|
4363
|
+
for (const nodeBindingPartial of nodeInfo.parseBindTextResults) {
|
|
4364
|
+
const nodeStateElement = getStateElementByName(rootNode, nodeBindingPartial.stateName);
|
|
4365
|
+
if (nodeStateElement === null) {
|
|
4366
|
+
raiseError(`State element with name "${nodeBindingPartial.stateName}" not found for fragment info node.`);
|
|
4367
|
+
}
|
|
4368
|
+
nodeStateElement.setPathInfo(nodeBindingPartial.statePathName, nodeBindingPartial.bindingType);
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
function getFragmentInfoByUUID(uuid) {
|
|
4374
|
+
return fragmentInfoByUUID.get(uuid) || null;
|
|
4375
|
+
}
|
|
4376
|
+
function getAllFragmentUUIDs() {
|
|
4377
|
+
return Array.from(fragmentInfoByUUID.keys());
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// SSR コメントパターン
|
|
4381
|
+
const SSR_PLACEHOLDER_COMMENT = /^@@wcs-(?:for|if|elseif|else):[^-]/;
|
|
4382
|
+
const SSR_BLOCK_START = /^@@wcs-(for|if|elseif|else)-start:(.+)$/;
|
|
4383
|
+
const SSR_BLOCK_END = /^@@wcs-(for|if|elseif|else)-end:(.+)$/;
|
|
4384
|
+
const SSR_TEXT_START = /^@@wcs-text-start:(.+)$/;
|
|
4385
|
+
class Ssr extends HTMLElement {
|
|
4386
|
+
_stateData = null;
|
|
4387
|
+
_templates = null;
|
|
4388
|
+
_hydrateProps = null;
|
|
4389
|
+
get name() {
|
|
4390
|
+
return this.getAttribute('name') || 'default';
|
|
4391
|
+
}
|
|
4392
|
+
get version() {
|
|
4393
|
+
return this.getAttribute('version') || '';
|
|
4394
|
+
}
|
|
4395
|
+
get stateData() {
|
|
4396
|
+
if (this._stateData === null) {
|
|
4397
|
+
this._stateData = this._loadStateData();
|
|
4398
|
+
}
|
|
4399
|
+
return this._stateData;
|
|
4400
|
+
}
|
|
4401
|
+
get templates() {
|
|
4402
|
+
if (this._templates === null) {
|
|
4403
|
+
this._templates = this._loadTemplates();
|
|
4404
|
+
}
|
|
4405
|
+
return this._templates;
|
|
4406
|
+
}
|
|
4407
|
+
get hydrateProps() {
|
|
4408
|
+
if (this._hydrateProps === null) {
|
|
4409
|
+
this._hydrateProps = this._loadHydrateProps();
|
|
4410
|
+
}
|
|
4411
|
+
return this._hydrateProps;
|
|
4412
|
+
}
|
|
4413
|
+
getTemplate(uuid) {
|
|
4414
|
+
return this.templates.get(uuid) ?? null;
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* サーバーの SSR バージョンとクライアントの state バージョンを検証する。
|
|
4418
|
+
* メジャー・マイナーバージョンが一致すればtrue。
|
|
4419
|
+
* version 属性がない場合は検証スキップ(true)。
|
|
4420
|
+
*/
|
|
4421
|
+
verifyVersion() {
|
|
4422
|
+
const serverVersion = this.version;
|
|
4423
|
+
if (!serverVersion)
|
|
4424
|
+
return true;
|
|
4425
|
+
const serverParts = serverVersion.split('.');
|
|
4426
|
+
const clientParts = VERSION.split('.');
|
|
4427
|
+
// メジャー・マイナーが一致すれば互換
|
|
4428
|
+
return serverParts[0] === clientParts[0] && serverParts[1] === clientParts[1];
|
|
4429
|
+
}
|
|
4430
|
+
setStateData(data) {
|
|
4431
|
+
this._stateData = data;
|
|
4432
|
+
}
|
|
4433
|
+
setHydrateProps(props) {
|
|
4434
|
+
this._hydrateProps = props;
|
|
4435
|
+
}
|
|
4436
|
+
_loadStateData() {
|
|
4437
|
+
const script = this.querySelector(`script[type="application/json"]:not([data-wcs-ssr-props])`);
|
|
4438
|
+
if (!script)
|
|
4439
|
+
return {};
|
|
4440
|
+
try {
|
|
4441
|
+
return JSON.parse(script.textContent || '{}');
|
|
4442
|
+
}
|
|
4443
|
+
catch {
|
|
4444
|
+
return {};
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
_loadTemplates() {
|
|
4448
|
+
const map = new Map();
|
|
4449
|
+
const templates = this.querySelectorAll('template[id]');
|
|
4450
|
+
for (const tpl of templates) {
|
|
4451
|
+
const id = tpl.getAttribute('id');
|
|
4452
|
+
if (id) {
|
|
4453
|
+
map.set(id, tpl);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
return map;
|
|
4457
|
+
}
|
|
4458
|
+
_loadHydrateProps() {
|
|
4459
|
+
const script = this.querySelector('script[data-wcs-ssr-props]');
|
|
4460
|
+
if (!script)
|
|
4461
|
+
return {};
|
|
4462
|
+
try {
|
|
4463
|
+
return JSON.parse(script.textContent || '{}');
|
|
4464
|
+
}
|
|
4465
|
+
catch {
|
|
4466
|
+
return {};
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
static findByName(root, name) {
|
|
4470
|
+
const tagName = config.tagNames.ssr;
|
|
4471
|
+
const parentEl = root instanceof Element
|
|
4472
|
+
? root
|
|
4473
|
+
: root instanceof Document
|
|
4474
|
+
? root.documentElement
|
|
4475
|
+
: null;
|
|
4476
|
+
if (!parentEl)
|
|
4477
|
+
return null;
|
|
4478
|
+
const el = parentEl.querySelector(`${tagName}[name="${name}"]`);
|
|
4479
|
+
return el;
|
|
4480
|
+
}
|
|
4481
|
+
/**
|
|
4482
|
+
* stateData と構造テンプレート・プロパティから <wcs-ssr> の中身を構築する。
|
|
4483
|
+
* server パッケージの renderToString から呼ばれる。
|
|
4484
|
+
*/
|
|
4485
|
+
/**
|
|
4486
|
+
* wcs-state 要素から $ プレフィックスや関数を除いたデータを抽出する。
|
|
4487
|
+
*/
|
|
4488
|
+
static extractStateData(stateEl) {
|
|
4489
|
+
const raw = stateEl.__state;
|
|
4490
|
+
if (!raw || typeof raw !== 'object')
|
|
4491
|
+
return {};
|
|
4492
|
+
const data = {};
|
|
4493
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
4494
|
+
if (!key.startsWith('$') && typeof value !== 'function') {
|
|
4495
|
+
data[key] = value;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
return data;
|
|
4499
|
+
}
|
|
4500
|
+
static buildContent(ssrEl, stateData) {
|
|
4501
|
+
// 初期データ JSON
|
|
4502
|
+
const jsonScript = document.createElement('script');
|
|
4503
|
+
jsonScript.setAttribute('type', 'application/json');
|
|
4504
|
+
jsonScript.textContent = JSON.stringify(stateData);
|
|
4505
|
+
ssrEl.appendChild(jsonScript);
|
|
4506
|
+
// UUID で管理されているテンプレートを復元して格納
|
|
4507
|
+
const uuids = getAllFragmentUUIDs();
|
|
4508
|
+
for (const uuid of uuids) {
|
|
4509
|
+
const fragmentInfo = getFragmentInfoByUUID(uuid);
|
|
4510
|
+
if (!fragmentInfo)
|
|
4511
|
+
continue;
|
|
4512
|
+
const tpl = document.createElement('template');
|
|
4513
|
+
tpl.setAttribute('id', uuid);
|
|
4514
|
+
const bindResult = fragmentInfo.parseBindTextResult;
|
|
4515
|
+
const bindText = bindResult.bindingType === 'else'
|
|
4516
|
+
? 'else:'
|
|
4517
|
+
: `${bindResult.bindingType}: ${bindResult.statePathName}`;
|
|
4518
|
+
tpl.setAttribute(config.bindAttributeName, bindText);
|
|
4519
|
+
const content = fragmentInfo.fragment.cloneNode(true);
|
|
4520
|
+
tpl.content.appendChild(content);
|
|
4521
|
+
ssrEl.appendChild(tpl);
|
|
4522
|
+
}
|
|
4523
|
+
// 属性で代替不可なプロパティをハイドレーション用に格納
|
|
4524
|
+
const ssrNodes = getAllSsrPropertyNodes();
|
|
4525
|
+
if (ssrNodes.length > 0) {
|
|
4526
|
+
const propsData = {};
|
|
4527
|
+
for (let i = 0; i < ssrNodes.length; i++) {
|
|
4528
|
+
const node = ssrNodes[i];
|
|
4529
|
+
const entries = getSsrProperties(node);
|
|
4530
|
+
if (entries.length === 0)
|
|
4531
|
+
continue;
|
|
4532
|
+
const id = `wcs-ssr-${i}`;
|
|
4533
|
+
node.setAttribute('data-wcs-ssr-id', id);
|
|
4534
|
+
const props = {};
|
|
4535
|
+
for (const entry of entries) {
|
|
4536
|
+
props[entry.propName] = entry.value;
|
|
4537
|
+
}
|
|
4538
|
+
propsData[id] = props;
|
|
4539
|
+
}
|
|
4540
|
+
if (Object.keys(propsData).length > 0) {
|
|
4541
|
+
const propsScript = document.createElement('script');
|
|
4542
|
+
propsScript.setAttribute('type', 'application/json');
|
|
4543
|
+
propsScript.setAttribute('data-wcs-ssr-props', '');
|
|
4544
|
+
propsScript.textContent = JSON.stringify(propsData);
|
|
4545
|
+
ssrEl.appendChild(propsScript);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
clearSsrPropertyStore();
|
|
4549
|
+
}
|
|
4550
|
+
/**
|
|
4551
|
+
* SSR ブロック境界コメント (@@wcs-*-start/end) を除去する
|
|
4552
|
+
*/
|
|
4553
|
+
static removeBlockBoundaryComments(root) {
|
|
4554
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
4555
|
+
const toRemove = [];
|
|
4556
|
+
while (walker.nextNode()) {
|
|
4557
|
+
const comment = walker.currentNode;
|
|
4558
|
+
if (SSR_BLOCK_START.test(comment.data) || SSR_BLOCK_END.test(comment.data)) {
|
|
4559
|
+
toRemove.push(comment);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
for (const comment of toRemove) {
|
|
4563
|
+
comment.remove();
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
/**
|
|
4567
|
+
* SSR の構造プレースホルダーコメント (@@wcs-for:uuid 等) を除去する
|
|
4568
|
+
*/
|
|
4569
|
+
static removeStructuralComments(root) {
|
|
4570
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
4571
|
+
const toRemove = [];
|
|
4572
|
+
while (walker.nextNode()) {
|
|
4573
|
+
const comment = walker.currentNode;
|
|
4574
|
+
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
4575
|
+
toRemove.push(comment);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
for (const comment of toRemove) {
|
|
4579
|
+
comment.remove();
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
/**
|
|
4583
|
+
* SSR テキストバインディングコメントを復元する。
|
|
4584
|
+
* <!--@@wcs-text-start:path-->text<!--@@wcs-text-end:path-->
|
|
4585
|
+
* → <!--@@: path--> (バインディングシステムが認識する形式)
|
|
4586
|
+
*/
|
|
4587
|
+
static restoreTextBindings(root) {
|
|
4588
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
4589
|
+
const startComments = [];
|
|
4590
|
+
while (walker.nextNode()) {
|
|
4591
|
+
const comment = walker.currentNode;
|
|
4592
|
+
const match = SSR_TEXT_START.exec(comment.data);
|
|
4593
|
+
if (match) {
|
|
4594
|
+
startComments.push({ comment, path: match[1] });
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
for (const { comment, path } of startComments) {
|
|
4598
|
+
const bindComment = document.createComment(`@@: ${path}`);
|
|
4599
|
+
comment.parentNode.insertBefore(bindComment, comment);
|
|
4600
|
+
let sibling = comment.nextSibling;
|
|
4601
|
+
comment.remove();
|
|
4602
|
+
const endPattern = `@@wcs-text-end:${path}`;
|
|
4603
|
+
while (sibling) {
|
|
4604
|
+
const next = sibling.nextSibling;
|
|
4605
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
4606
|
+
sibling.parentNode.removeChild(sibling);
|
|
4607
|
+
break;
|
|
4608
|
+
}
|
|
4609
|
+
sibling.parentNode.removeChild(sibling);
|
|
4610
|
+
sibling = next;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
/**
|
|
4615
|
+
* SSR DOM をクリーンアップし、buildBindings が動作できる状態に戻す。
|
|
4616
|
+
* バージョン不一致時のフォールバック用。
|
|
4617
|
+
*
|
|
4618
|
+
* 1. SSR ブロック境界コメント間のレンダリング済みノードを除去
|
|
4619
|
+
* 2. SSR テキストバインディングを @@: 形式に復元
|
|
4620
|
+
* 3. プレースホルダーコメントを <wcs-ssr> 内のテンプレートで差し替え
|
|
4621
|
+
* 4. data-wcs-ssr-id 属性を除去
|
|
4622
|
+
* 5. <wcs-ssr> を除去
|
|
4623
|
+
*/
|
|
4624
|
+
static cleanupDom(root) {
|
|
4625
|
+
const body = document.body;
|
|
4626
|
+
// <wcs-ssr> からテンプレート UUID マップを構築(カスタム要素未定義でも動作するよう DOM 直接走査)
|
|
4627
|
+
const ssrElements = root.querySelectorAll(config.tagNames.ssr);
|
|
4628
|
+
const templateByUuid = new Map();
|
|
4629
|
+
for (const ssrNode of ssrElements) {
|
|
4630
|
+
const templates = ssrNode.querySelectorAll('template[id]');
|
|
4631
|
+
for (const tpl of templates) {
|
|
4632
|
+
const id = tpl.getAttribute('id');
|
|
4633
|
+
if (id) {
|
|
4634
|
+
templateByUuid.set(id, tpl);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
// SSR ブロック境界コメント間のレンダリング済みノードと境界コメントを除去
|
|
4639
|
+
const walker1 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
4640
|
+
const startComments = [];
|
|
4641
|
+
while (walker1.nextNode()) {
|
|
4642
|
+
const comment = walker1.currentNode;
|
|
4643
|
+
if (SSR_BLOCK_START.test(comment.data)) {
|
|
4644
|
+
startComments.push(comment);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
for (const startComment of startComments) {
|
|
4648
|
+
const match = SSR_BLOCK_START.exec(startComment.data);
|
|
4649
|
+
const type = match[1];
|
|
4650
|
+
const info = match[2];
|
|
4651
|
+
const endPattern = `@@wcs-${type}-end:${info}`;
|
|
4652
|
+
let sibling = startComment.nextSibling;
|
|
4653
|
+
while (sibling) {
|
|
4654
|
+
const next = sibling.nextSibling;
|
|
4655
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.data === endPattern) {
|
|
4656
|
+
sibling.remove();
|
|
4657
|
+
break;
|
|
4658
|
+
}
|
|
4659
|
+
sibling.remove();
|
|
4660
|
+
sibling = next;
|
|
4661
|
+
}
|
|
4662
|
+
startComment.remove();
|
|
4663
|
+
}
|
|
4664
|
+
// SSR テキストバインディングを @@: 形式に復元
|
|
4665
|
+
Ssr.restoreTextBindings(body);
|
|
4666
|
+
// プレースホルダーコメント (@@wcs-for:uuid 等) をテンプレートに差し替え
|
|
4667
|
+
const walker2 = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT);
|
|
4668
|
+
const placeholders = [];
|
|
4669
|
+
while (walker2.nextNode()) {
|
|
4670
|
+
const comment = walker2.currentNode;
|
|
4671
|
+
if (SSR_PLACEHOLDER_COMMENT.test(comment.data)) {
|
|
4672
|
+
const uuid = comment.data.split(':')[1];
|
|
4673
|
+
placeholders.push({ comment, uuid });
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
for (const { comment, uuid } of placeholders) {
|
|
4677
|
+
const tpl = templateByUuid.get(uuid);
|
|
4678
|
+
if (tpl) {
|
|
4679
|
+
const restored = document.createElement('template');
|
|
4680
|
+
const bindAttr = tpl.getAttribute(config.bindAttributeName);
|
|
4681
|
+
if (bindAttr)
|
|
4682
|
+
restored.setAttribute(config.bindAttributeName, bindAttr);
|
|
4683
|
+
const imported = document.importNode(tpl.content, true);
|
|
4684
|
+
if (imported.childNodes.length > 0) {
|
|
4685
|
+
restored.content.appendChild(imported);
|
|
4686
|
+
}
|
|
4687
|
+
else {
|
|
4688
|
+
for (const child of Array.from(tpl.childNodes)) {
|
|
4689
|
+
restored.content.appendChild(document.importNode(child, true));
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
comment.parentNode.replaceChild(restored, comment);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
// data-wcs-ssr-id 属性を除去
|
|
4696
|
+
const ssrIdElements = root.querySelectorAll('[data-wcs-ssr-id]');
|
|
4697
|
+
for (const el of ssrIdElements) {
|
|
4698
|
+
el.removeAttribute('data-wcs-ssr-id');
|
|
4699
|
+
}
|
|
4700
|
+
// <wcs-ssr> を除去
|
|
4701
|
+
for (const el of ssrElements) {
|
|
4702
|
+
el.remove();
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
async function loadFromInnerScript(script, name) {
|
|
4708
|
+
let scriptModule = null;
|
|
4709
|
+
const uniq_comment = `\n//# sourceURL=${name}\n`;
|
|
4710
|
+
if (typeof URL.createObjectURL === 'function') {
|
|
4711
|
+
// Create a blob URL for the script and dynamically import it
|
|
4712
|
+
const blob = new Blob([script.text + uniq_comment], { type: "application/javascript" });
|
|
4713
|
+
const url = URL.createObjectURL(blob);
|
|
4714
|
+
try {
|
|
4715
|
+
scriptModule = await import(url);
|
|
4716
|
+
}
|
|
4717
|
+
finally {
|
|
4718
|
+
// Clean up blob URL to prevent memory leak
|
|
4719
|
+
URL.revokeObjectURL(url);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
else {
|
|
4723
|
+
// Fallback: Base64 encoding method (for test environment)
|
|
4724
|
+
// Convert script to Base64 and import via data: URL
|
|
4725
|
+
const b64 = btoa(String.fromCodePoint(...new TextEncoder().encode(script.text + uniq_comment)));
|
|
4726
|
+
scriptModule = await import(`data:application/javascript;base64,${b64}`);
|
|
4727
|
+
}
|
|
4728
|
+
return (scriptModule && typeof scriptModule.default === 'object') ? scriptModule.default : {};
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
async function loadFromJsonFile(url) {
|
|
4732
|
+
try {
|
|
4733
|
+
const response = await fetch(url);
|
|
4734
|
+
if (!response.ok) {
|
|
4735
|
+
raiseError(`Failed to fetch JSON file: ${response.statusText}`);
|
|
4736
|
+
}
|
|
4737
|
+
const data = await response.json();
|
|
4738
|
+
return data;
|
|
4739
|
+
}
|
|
4740
|
+
catch (e) {
|
|
4741
|
+
console.error('Failed to load JSON file:', e);
|
|
4742
|
+
return {};
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
async function loadFromScriptFile(url) {
|
|
4747
|
+
try {
|
|
4748
|
+
const module = await import(/* @vite-ignore */ url);
|
|
4749
|
+
return module.default || {};
|
|
4750
|
+
}
|
|
4751
|
+
catch (e) {
|
|
4752
|
+
raiseError(`Failed to load script file: ${e}`);
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
function loadFromScriptJson(id) {
|
|
4757
|
+
const script = document.getElementById(id);
|
|
4758
|
+
if (script && script.type === 'application/json') {
|
|
4759
|
+
try {
|
|
4760
|
+
const data = JSON.parse(script.textContent || '{}');
|
|
4761
|
+
return data;
|
|
4762
|
+
}
|
|
4763
|
+
catch (e) {
|
|
4764
|
+
raiseError('Failed to parse JSON from script element:' + e);
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
return {};
|
|
4768
|
+
}
|
|
4769
|
+
|
|
3859
4770
|
class LoopContextStack {
|
|
3860
4771
|
_loopContextStack = Array(MAX_LOOP_DEPTH).fill(undefined);
|
|
3861
4772
|
_length = 0;
|
|
@@ -5531,11 +6442,14 @@ function getStateInfo(state) {
|
|
|
5531
6442
|
};
|
|
5532
6443
|
}
|
|
5533
6444
|
class State extends HTMLElement {
|
|
6445
|
+
static hasConnectedCallbackPromise = true;
|
|
5534
6446
|
__state;
|
|
5535
6447
|
_name = 'default';
|
|
5536
6448
|
_initialized = false;
|
|
5537
6449
|
_initializePromise;
|
|
5538
6450
|
_resolveInitialize = null;
|
|
6451
|
+
_connectedCallbackPromise;
|
|
6452
|
+
_resolveConnectedCallback = null;
|
|
5539
6453
|
_loadingPromise;
|
|
5540
6454
|
_resolveLoading = null;
|
|
5541
6455
|
_setStatePromise = null;
|
|
@@ -5557,6 +6471,9 @@ class State extends HTMLElement {
|
|
|
5557
6471
|
this._initializePromise = new Promise((resolve) => {
|
|
5558
6472
|
this._resolveInitialize = resolve;
|
|
5559
6473
|
});
|
|
6474
|
+
this._connectedCallbackPromise = new Promise((resolve) => {
|
|
6475
|
+
this._resolveConnectedCallback = resolve;
|
|
6476
|
+
});
|
|
5560
6477
|
this._loadingPromise = new Promise((resolve) => {
|
|
5561
6478
|
this._resolveLoading = resolve;
|
|
5562
6479
|
});
|
|
@@ -5588,7 +6505,22 @@ class State extends HTMLElement {
|
|
|
5588
6505
|
get name() {
|
|
5589
6506
|
return this._name;
|
|
5590
6507
|
}
|
|
6508
|
+
_loadFromSsrElement() {
|
|
6509
|
+
if (!this.hasAttribute('enable-ssr'))
|
|
6510
|
+
return null;
|
|
6511
|
+
const name = this.getAttribute('name') || 'default';
|
|
6512
|
+
const root = this.parentNode;
|
|
6513
|
+
if (!root)
|
|
6514
|
+
return null;
|
|
6515
|
+
const ssrEl = Ssr.findByName(root, name);
|
|
6516
|
+
if (!ssrEl)
|
|
6517
|
+
return null;
|
|
6518
|
+
const data = ssrEl.stateData;
|
|
6519
|
+
return Object.keys(data).length > 0 ? data : null;
|
|
6520
|
+
}
|
|
5591
6521
|
async _initialize() {
|
|
6522
|
+
// enable-ssr (クライアント側のみ): <wcs-ssr> から初期データを取得
|
|
6523
|
+
const ssrState = !inSsr() ? this._loadFromSsrElement() : null;
|
|
5592
6524
|
try {
|
|
5593
6525
|
if (this.hasAttribute('state')) {
|
|
5594
6526
|
const state = this.getAttribute('state');
|
|
@@ -5628,6 +6560,21 @@ class State extends HTMLElement {
|
|
|
5628
6560
|
catch (e) {
|
|
5629
6561
|
raiseError(`Failed to initialize state: ${e}`);
|
|
5630
6562
|
}
|
|
6563
|
+
// SSR データがある場合、state 定義(メソッド/getter)を維持しつつデータ値を上書き
|
|
6564
|
+
if (ssrState !== null && this.__state) {
|
|
6565
|
+
for (const [key, value] of Object.entries(ssrState)) {
|
|
6566
|
+
if (key in this.__state) {
|
|
6567
|
+
const desc = Object.getOwnPropertyDescriptor(this.__state, key);
|
|
6568
|
+
// getter/setter はスキップ(定義側を優先)
|
|
6569
|
+
if (desc && (desc.get || desc.set))
|
|
6570
|
+
continue;
|
|
6571
|
+
// 関数はスキップ
|
|
6572
|
+
if (typeof this.__state[key] === 'function')
|
|
6573
|
+
continue;
|
|
6574
|
+
}
|
|
6575
|
+
this.__state[key] = value;
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
5631
6578
|
await this._loadingPromise;
|
|
5632
6579
|
this._name = this.getAttribute('name') || 'default';
|
|
5633
6580
|
setStateElementByName(this.rootNode, this._name, this);
|
|
@@ -5693,7 +6640,23 @@ class State extends HTMLElement {
|
|
|
5693
6640
|
this._initialized = true;
|
|
5694
6641
|
this._resolveInitialize?.();
|
|
5695
6642
|
}
|
|
5696
|
-
|
|
6643
|
+
// enable-ssr (クライアント側): SSR で $connectedCallback 済みなのでスキップ
|
|
6644
|
+
// inSsr() (サーバー側): レンダリング中なので実行する
|
|
6645
|
+
if (!this.hasAttribute('enable-ssr') || inSsr()) {
|
|
6646
|
+
await this._callStateConnectedCallback();
|
|
6647
|
+
}
|
|
6648
|
+
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成
|
|
6649
|
+
if (inSsr() && this.hasAttribute('enable-ssr')) {
|
|
6650
|
+
await getBindingsReady(this.rootNode);
|
|
6651
|
+
const name = this.getAttribute('name') || 'default';
|
|
6652
|
+
const stateData = Ssr.extractStateData(this);
|
|
6653
|
+
const ssrEl = document.createElement(config.tagNames.ssr);
|
|
6654
|
+
ssrEl.setAttribute('name', name);
|
|
6655
|
+
ssrEl.setAttribute('version', VERSION);
|
|
6656
|
+
Ssr.buildContent(ssrEl, stateData);
|
|
6657
|
+
this.parentNode?.insertBefore(ssrEl, this);
|
|
6658
|
+
}
|
|
6659
|
+
this._resolveConnectedCallback?.();
|
|
5697
6660
|
}
|
|
5698
6661
|
disconnectedCallback() {
|
|
5699
6662
|
if (this._rootNode !== null) {
|
|
@@ -5705,6 +6668,9 @@ class State extends HTMLElement {
|
|
|
5705
6668
|
get initializePromise() {
|
|
5706
6669
|
return this._initializePromise;
|
|
5707
6670
|
}
|
|
6671
|
+
get connectedCallbackPromise() {
|
|
6672
|
+
return this._connectedCallbackPromise;
|
|
6673
|
+
}
|
|
5708
6674
|
get listPaths() {
|
|
5709
6675
|
return this._listPaths;
|
|
5710
6676
|
}
|
|
@@ -5832,7 +6798,9 @@ class State extends HTMLElement {
|
|
|
5832
6798
|
}
|
|
5833
6799
|
|
|
5834
6800
|
function registerComponents() {
|
|
5835
|
-
|
|
6801
|
+
if (!customElements.get(config.tagNames.ssr)) {
|
|
6802
|
+
customElements.define(config.tagNames.ssr, Ssr);
|
|
6803
|
+
}
|
|
5836
6804
|
if (!customElements.get(config.tagNames.state)) {
|
|
5837
6805
|
customElements.define(config.tagNames.state, State);
|
|
5838
6806
|
}
|
|
@@ -5931,5 +6899,5 @@ function defineState(definition) {
|
|
|
5931
6899
|
return definition;
|
|
5932
6900
|
}
|
|
5933
6901
|
|
|
5934
|
-
export { bootstrapState, defineState };
|
|
6902
|
+
export { Ssr, VERSION, bootstrapState, buildBindings, defineState, getBindingsReady, getConfig };
|
|
5935
6903
|
//# sourceMappingURL=index.esm.js.map
|