@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/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
- async function loadFromInnerScript(script, name) {
51
- let scriptModule = null;
52
- const uniq_comment = `\n//# sourceURL=${name}\n`;
53
- if (typeof URL.createObjectURL === 'function') {
54
- // Create a blob URL for the script and dynamically import it
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
- content.mountAfter(bindingInfo.node);
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
- queueMicrotask(() => {
3840
- buildBindings(rootNode);
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
- queueMicrotask(() => {
3845
- buildBindings(rootNode);
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
- await this._callStateConnectedCallback();
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
- // Register custom element
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