@wcstack/server 0.2.3
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 +354 -0
- package/README.md +354 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.esm.js +265 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.esm.min.js.map +1 -0
- package/package.json +60 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# @wcstack/server
|
|
2
|
+
|
|
3
|
+
**Web Components がサーバーでレンダリングされたら?**
|
|
4
|
+
|
|
5
|
+
`<wcs-state>` テンプレートがブラウザに届く前に完全にレンダリングされる世界を想像してください。データは取得済み、バインディングは解決済み、リストは展開済み、条件分岐は評価済み。ユーザーは即座にコンテンツを目にし、クライアントはサーバーが中断した地点からシームレスに引き継ぎます。
|
|
6
|
+
|
|
7
|
+
`@wcstack/server` はそれを実現します。既存の `@wcstack/state` テンプレートを happy-dom 上で実行し、ハイドレーションデータを埋め込んだレンダリング済み HTML を生成。クライアントはフリッカーなしでリアクティビティを再開します。特別なテンプレート構文もサーバー専用のマークアップも不要 — いつも書いている HTML がそのまま使えます。
|
|
8
|
+
|
|
9
|
+
## 特徴
|
|
10
|
+
|
|
11
|
+
### 基本機能
|
|
12
|
+
- **テンプレートの完全レンダリング**: `@wcstack/state` のバインディングをサーバーサイドで実行 — テキスト、属性、`for` ループ、`if`/`elseif`/`else` 条件分岐、フィルタ、Mustache `{{ }}` 構文に対応
|
|
13
|
+
- **ハイドレーションデータの自動生成**: 状態スナップショット、テンプレートフラグメント、プロパティマップを含む `<wcs-ssr>` 要素を生成し、クライアント側でシームレスにハイドレーション
|
|
14
|
+
- **非同期データ取得**: `$connectedCallback` 内の `fetch()` に対応 — サーバーはすべての非同期処理の完了を待ってからレンダリング
|
|
15
|
+
- **RenderCore**: `wc-bindable` プロトコルに準拠したヘッドレスのイベント駆動レンダリングクラス。`html` / `loading` / `error` の状態を監視可能
|
|
16
|
+
- **ブラウザ依存ゼロ**: Node.js 上で動作し、ランタイム依存は happy-dom のみ
|
|
17
|
+
|
|
18
|
+
### ユニークな機能
|
|
19
|
+
- **ドロップイン SSR**: クライアント側テンプレートの変更不要。`<wcs-state>` に `enable-ssr` を追加して `renderToString()` で呼び出すだけ
|
|
20
|
+
- **テンプレートフラグメントの保存**: `for`/`if` テンプレートのソースを UUID 参照付きでキャプチャし、クライアント側で構造ディレクティブを再実行可能に
|
|
21
|
+
- **プロパティハイドレーション**: 属性では表現できない DOM プロパティ(`innerHTML` など)を個別にシリアライズし、ハイドレーション時に復元
|
|
22
|
+
- **wc-bindable プロトコル**: `RenderCore` は標準プロトコルでレンダリング状態を公開し、サーバーでもクライアントでも同じ `bind()` パターンで利用可能
|
|
23
|
+
|
|
24
|
+
## インストール
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @wcstack/server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## クイックスタート
|
|
31
|
+
|
|
32
|
+
### `renderToString()` — ワンショットレンダリング
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import { renderToString } from "@wcstack/server";
|
|
36
|
+
|
|
37
|
+
const html = await renderToString(`
|
|
38
|
+
<wcs-state json='{"items":["Apple","Banana","Cherry"]}' enable-ssr>
|
|
39
|
+
</wcs-state>
|
|
40
|
+
<ul>
|
|
41
|
+
<template data-wcs="for: items">
|
|
42
|
+
<li data-wcs="textContent: items.*"></li>
|
|
43
|
+
</template>
|
|
44
|
+
</ul>
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
console.log(html);
|
|
48
|
+
// ハイドレーションデータ付きのレンダリング済み HTML
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `RenderCore` — 監視可能なレンダリング(キャッシュ付き)
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { RenderCore } from "@wcstack/server";
|
|
55
|
+
|
|
56
|
+
const renderer = new RenderCore();
|
|
57
|
+
|
|
58
|
+
// wc-bindable プロトコル経由で状態変更をリッスン
|
|
59
|
+
renderer.addEventListener("wcs-render:loading-changed", (e) => {
|
|
60
|
+
console.log("loading:", e.detail);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
renderer.addEventListener("wcs-render:html-changed", (e) => {
|
|
64
|
+
console.log("rendered:", e.detail.length, "bytes");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// レンダリングしてキャッシュ
|
|
68
|
+
await renderer.render(templateHtml);
|
|
69
|
+
|
|
70
|
+
// 以降の読み取りはキャッシュを利用
|
|
71
|
+
console.log(renderer.html);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API リファレンス
|
|
75
|
+
|
|
76
|
+
### `renderToString(html: string): Promise<string>`
|
|
77
|
+
|
|
78
|
+
`@wcstack/state` テンプレートを含む HTML 文字列をレンダリングします。`<wcs-state enable-ssr>` を持つ要素のハイドレーションデータ付きのレンダリング済み HTML を返します。
|
|
79
|
+
|
|
80
|
+
**レンダリングパイプライン:**
|
|
81
|
+
1. happy-dom ウィンドウを作成し、ブラウザグローバルをインストール
|
|
82
|
+
2. HTML をパースし、すべての `<wcs-state>` 要素の `connectedCallback` を発火
|
|
83
|
+
3. すべての `$connectedCallback` プロミス(`fetch()` 呼び出し含む)の完了を待機
|
|
84
|
+
4. `buildBindings` の完了を待機
|
|
85
|
+
5. `enable-ssr` を持つ状態に `<wcs-ssr>` 要素を生成
|
|
86
|
+
6. グローバルを復元し、レンダリング済み HTML を返却
|
|
87
|
+
|
|
88
|
+
### `RenderCore`
|
|
89
|
+
|
|
90
|
+
`EventTarget` を継承したヘッドレスレンダリングクラス。`wc-bindable` プロトコルを実装。
|
|
91
|
+
|
|
92
|
+
| プロパティ | 型 | 説明 |
|
|
93
|
+
|----------|------|-------------|
|
|
94
|
+
| `html` | `string \| null` | レンダリング済み HTML(`render()` 後にキャッシュ) |
|
|
95
|
+
| `loading` | `boolean` | レンダリング中は `true` |
|
|
96
|
+
| `error` | `Error \| null` | 直前の `render()` のエラー(エラーがあれば) |
|
|
97
|
+
|
|
98
|
+
| メソッド | 戻り値 | 説明 |
|
|
99
|
+
|--------|---------|-------------|
|
|
100
|
+
| `render(html)` | `Promise<string \| null>` | テンプレートをレンダリングして結果をキャッシュ。エラー時は `null` を返却 |
|
|
101
|
+
|
|
102
|
+
| イベント | Detail | 説明 |
|
|
103
|
+
|-------|--------|-------------|
|
|
104
|
+
| `wcs-render:html-changed` | `string` | レンダリング成功時に発火 |
|
|
105
|
+
| `wcs-render:loading-changed` | `boolean` | ローディング状態の変更時に発火 |
|
|
106
|
+
| `wcs-render:error` | `Error` | レンダリング失敗時に発火 |
|
|
107
|
+
|
|
108
|
+
**wc-bindable 宣言:**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
static wcBindable = {
|
|
112
|
+
protocol: "wc-bindable",
|
|
113
|
+
version: 1,
|
|
114
|
+
properties: [
|
|
115
|
+
{ name: "html", event: "wcs-render:html-changed" },
|
|
116
|
+
{ name: "loading", event: "wcs-render:loading-changed" },
|
|
117
|
+
{ name: "error", event: "wcs-render:error" },
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### ヘルパー関数
|
|
123
|
+
|
|
124
|
+
| 関数 | 説明 |
|
|
125
|
+
|----------|-------------|
|
|
126
|
+
| `installGlobals(window)` | happy-dom のグローバルを `globalThis` にインストール。復元関数を返す |
|
|
127
|
+
| `extractStateData(stateEl)` | `<wcs-state>` 要素からデータプロパティを抽出(`$` プレフィックスのキーと関数は除外) |
|
|
128
|
+
|
|
129
|
+
### 定数
|
|
130
|
+
|
|
131
|
+
| 名前 | 説明 |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `GLOBALS_KEYS` | SSR 中にインストールされるブラウザグローバルキーの配列(`document`、`HTMLElement`、`Node` 等) |
|
|
134
|
+
| `VERSION` | `package.json` から取得したパッケージバージョン文字列 |
|
|
135
|
+
|
|
136
|
+
## SSR 出力構造
|
|
137
|
+
|
|
138
|
+
`<wcs-state>` に `enable-ssr` 属性がある場合、`renderToString()` はその直前にハイドレーションデータを含む `<wcs-ssr>` 要素を挿入します:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<!-- renderToString() が生成 -->
|
|
142
|
+
<wcs-ssr name="default" version="0.1.0">
|
|
143
|
+
|
|
144
|
+
<!-- 状態スナップショット -->
|
|
145
|
+
<script type="application/json">{"items":["Apple","Banana","Cherry"]}</script>
|
|
146
|
+
|
|
147
|
+
<!-- テンプレートフラグメント(クライアント側での再実行用) -->
|
|
148
|
+
<template id="uuid-1234" data-wcs="for: items">
|
|
149
|
+
<li data-wcs="textContent: items.*"></li>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<!-- 属性で代替不可なプロパティ(オプション) -->
|
|
153
|
+
<script type="application/json" data-wcs-ssr-props>
|
|
154
|
+
{"wcs-ssr-0": {"innerHTML": "<b>rich</b>"}}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
</wcs-ssr>
|
|
158
|
+
|
|
159
|
+
<wcs-state json='...' enable-ssr></wcs-state>
|
|
160
|
+
|
|
161
|
+
<!-- レンダリング済み出力(即座に表示) -->
|
|
162
|
+
<ul>
|
|
163
|
+
<li>Apple</li>
|
|
164
|
+
<li>Banana</li>
|
|
165
|
+
<li>Cherry</li>
|
|
166
|
+
</ul>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
クライアント側の `@wcstack/state` はハイドレーション時に `<wcs-ssr>` 要素を読み取り、状態とテンプレートを復元し、再レンダリングなしでリアクティビティを再開します。
|
|
170
|
+
|
|
171
|
+
## サーバー統合の例
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import { createServer } from "node:http";
|
|
175
|
+
import { RenderCore } from "@wcstack/server";
|
|
176
|
+
|
|
177
|
+
const renderer = new RenderCore();
|
|
178
|
+
|
|
179
|
+
const template = `
|
|
180
|
+
<wcs-state enable-ssr>
|
|
181
|
+
<script type="module">
|
|
182
|
+
export default {
|
|
183
|
+
async $connectedCallback() {
|
|
184
|
+
const res = await fetch("http://localhost:3000/api/data");
|
|
185
|
+
this.items = await res.json();
|
|
186
|
+
},
|
|
187
|
+
items: []
|
|
188
|
+
};
|
|
189
|
+
</script>
|
|
190
|
+
</wcs-state>
|
|
191
|
+
<ul>
|
|
192
|
+
<template data-wcs="for: items">
|
|
193
|
+
<li data-wcs="textContent: items.*"></li>
|
|
194
|
+
</template>
|
|
195
|
+
</ul>
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
createServer(async (req, res) => {
|
|
199
|
+
if (!renderer.html) {
|
|
200
|
+
await renderer.render(template);
|
|
201
|
+
}
|
|
202
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
203
|
+
res.end(renderer.html);
|
|
204
|
+
}).listen(3000);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 入力 HTML のルール
|
|
208
|
+
|
|
209
|
+
- `<body>` の中身だけを渡す(`<html>`, `<head>`, `<body>` タグは含めない)
|
|
210
|
+
- `<script>` / `<link>` による外部リソース読み込みは実行されない
|
|
211
|
+
→ 必要なパッケージは `options.bootstraps` で明示的に渡す
|
|
212
|
+
|
|
213
|
+
## SSR でできること
|
|
214
|
+
|
|
215
|
+
### 状態の初期化とデータ取得
|
|
216
|
+
|
|
217
|
+
- `<wcs-state>` の状態ロード(json 属性, src 属性, inline `<script type="module">`)
|
|
218
|
+
- `$connectedCallback` でのサーバーサイド fetch(API 呼び出し等)
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<!-- JSON 直接指定 -->
|
|
222
|
+
<wcs-state enable-ssr json='{"title":"Hello"}'></wcs-state>
|
|
223
|
+
|
|
224
|
+
<!-- $connectedCallback で API からデータ取得 -->
|
|
225
|
+
<!-- $connectedCallback は状態オブジェクトのメソッドとして定義、this が state proxy -->
|
|
226
|
+
<wcs-state enable-ssr>
|
|
227
|
+
<script type="module">
|
|
228
|
+
export default {
|
|
229
|
+
async $connectedCallback() {
|
|
230
|
+
const res = await fetch('/api/users');
|
|
231
|
+
this.users = await res.json();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
</script>
|
|
235
|
+
</wcs-state>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### wcs-fetch を使ったサーバー通信
|
|
239
|
+
|
|
240
|
+
- `<wcs-fetch>` の auto-fetch(`manual` なし)はサーバーでも実行される
|
|
241
|
+
- `manual` + `$connectedCallback` で明示的に制御する場合:
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<wcs-fetch id="api" url="/api/users" manual></wcs-fetch>
|
|
245
|
+
<wcs-state enable-ssr>
|
|
246
|
+
<script type="module">
|
|
247
|
+
export default {
|
|
248
|
+
async $connectedCallback() {
|
|
249
|
+
const el = document.getElementById('api');
|
|
250
|
+
this.users = await el.fetch();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
</script>
|
|
254
|
+
</wcs-state>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> ※ `bootstraps` オプションに `bootstrapFetch` を含める必要あり
|
|
258
|
+
|
|
259
|
+
### バインディングと構造レンダリング
|
|
260
|
+
|
|
261
|
+
- `data-wcs` バインディングの適用(text, attribute, class, style, property)
|
|
262
|
+
- `<template data-wcs="for:">` / `if:` / `elseif:` / `else:` の構造レンダリング
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<ul>
|
|
266
|
+
<template data-wcs="for: users">
|
|
267
|
+
<li data-wcs="textContent: .name"></li>
|
|
268
|
+
</template>
|
|
269
|
+
</ul>
|
|
270
|
+
<template data-wcs="if: isAdmin">
|
|
271
|
+
<div class="admin-panel">...</div>
|
|
272
|
+
</template>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### ハイドレーション
|
|
276
|
+
|
|
277
|
+
- `enable-ssr` 付き `<wcs-state>` の `<wcs-ssr>` メタデータ自動生成
|
|
278
|
+
- クライアント側でのハイドレーション(再レンダリングなしでバインディング復元)
|
|
279
|
+
- `enable-ssr` を外した `<wcs-state>` はクライアントのみで動作(部分 CSR)
|
|
280
|
+
|
|
281
|
+
### カスタム要素の待機
|
|
282
|
+
|
|
283
|
+
- `static hasConnectedCallbackPromise = true` プロトコル準拠の全カスタム要素を自動待機
|
|
284
|
+
|
|
285
|
+
## SSR でできないこと
|
|
286
|
+
|
|
287
|
+
- `<head>` 内の `<script src="...">` や `<link>` の自動実行
|
|
288
|
+
- ブラウザ固有 API(localStorage, sessionStorage, navigator 等)
|
|
289
|
+
- Shadow DOM のレンダリング(Declarative Shadow DOM 非対応)
|
|
290
|
+
- イベントハンドラの登録(クライアント側のハイドレーションで復元)
|
|
291
|
+
- `<wcs-autoloader>` による動的コンポーネント読み込み
|
|
292
|
+
|
|
293
|
+
## HTML の分割パターン
|
|
294
|
+
|
|
295
|
+
`renderToString` には `<body>` の中身だけを渡し、`<head>` や `<script>` タグは外側のテンプレートで囲む:
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
// server.js
|
|
299
|
+
const ssrBody = await renderToString(template, {
|
|
300
|
+
baseUrl: 'http://localhost:3001',
|
|
301
|
+
});
|
|
302
|
+
const page = `<!DOCTYPE html>
|
|
303
|
+
<html lang="ja">
|
|
304
|
+
<head>
|
|
305
|
+
<script type="module" src="/packages/state/dist/auto.js"></script>
|
|
306
|
+
</head>
|
|
307
|
+
<body>${ssrBody}</body>
|
|
308
|
+
</html>`;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 複数パッケージを使う場合
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
import { bootstrapState, getBindingsReady } from '@wcstack/state';
|
|
315
|
+
import { bootstrapFetch } from '@wcstack/fetch';
|
|
316
|
+
|
|
317
|
+
const ssrBody = await renderToString(template, {
|
|
318
|
+
baseUrl: 'http://localhost:3001',
|
|
319
|
+
bootstraps: [bootstrapState, bootstrapFetch],
|
|
320
|
+
ready: [(doc) => getBindingsReady(doc)],
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## 仕組み
|
|
325
|
+
|
|
326
|
+
### レンダリングパイプライン
|
|
327
|
+
|
|
328
|
+
1. **グローバルのセットアップ**: happy-dom の `Window` を作成し、ブラウザグローバル(`document`、`HTMLElement`、`MutationObserver` 等)を `globalThis` に一時的にインストール。`URL.createObjectURL` を無効化し、インラインスクリプトの base64 data URL フォールバックを強制。
|
|
329
|
+
|
|
330
|
+
2. **SSR モード**: `<html>` 要素に `data-wcs-server` 属性を設定。`@wcstack/state` はこの属性を検出して SSR 動作を有効化。
|
|
331
|
+
|
|
332
|
+
3. **ブートストラップ**: ユーザー提供の bootstrap 関数を呼び出す(省略時は `bootstrapState()` をデフォルト使用)。
|
|
333
|
+
|
|
334
|
+
4. **HTML パースとコールバック**: `document.body.innerHTML` に HTML をセットすることで、happy-dom の要素ライフサイクルが発火。各 `<wcs-state>` がデータソースをロードし `$connectedCallback` を実行。`hasConnectedCallbackPromise` を持つ全カスタム要素を待機。
|
|
335
|
+
|
|
336
|
+
5. **Ready**: ユーザー提供の ready 関数を待機(デフォルトは `getBindingsReady()`) — テキスト補間、属性マッピング、リスト展開、条件評価。
|
|
337
|
+
|
|
338
|
+
6. **SSR メタデータ**: 各 `<wcs-state enable-ssr>` が `connectedCallback` 内で自動的に `<wcs-ssr>` 要素を生成。
|
|
339
|
+
|
|
340
|
+
7. **クリーンアップ**: 元のグローバルを復元し、happy-dom ウィンドウを閉じる。
|
|
341
|
+
|
|
342
|
+
### クライアント側のハイドレーション
|
|
343
|
+
|
|
344
|
+
クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を検出し、以下を行います:
|
|
345
|
+
1. JSON スナップショットから状態を復元(ネットワークリクエストをスキップ)
|
|
346
|
+
2. UUID 参照を使ってテンプレートフラグメントを再接続
|
|
347
|
+
3. props スクリプトから属性で代替不可なプロパティを適用
|
|
348
|
+
4. 通常のリアクティブバインディングを再開
|
|
349
|
+
|
|
350
|
+
レンダリング済みの DOM は即座に表示されます — ハイドレーションはインタラクティビティの復元のみを行います。
|
|
351
|
+
|
|
352
|
+
## ライセンス
|
|
353
|
+
|
|
354
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# @wcstack/server
|
|
2
|
+
|
|
3
|
+
**What if Web Components rendered on the server?**
|
|
4
|
+
|
|
5
|
+
Imagine a future where your `<wcs-state>` templates are fully rendered before they reach the browser — data is fetched, bindings are resolved, lists are expanded, conditionals are evaluated. The user sees content instantly, and the client picks up exactly where the server left off.
|
|
6
|
+
|
|
7
|
+
That's what `@wcstack/server` explores. It runs your existing `@wcstack/state` templates through happy-dom, produces fully-rendered HTML with embedded hydration data, and lets the client resume reactivity with zero flicker. No special template syntax, no server-specific markup — just the same HTML you already write.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Basic Features
|
|
12
|
+
- **Full Template Rendering**: Runs `@wcstack/state` bindings server-side — text, attributes, `for` loops, `if`/`elseif`/`else` conditionals, filters, and mustache `{{ }}` syntax.
|
|
13
|
+
- **Automatic Hydration Data**: Generates `<wcs-ssr>` elements containing state snapshots, template fragments, and property maps for seamless client-side hydration.
|
|
14
|
+
- **Async Data Fetching**: Supports `$connectedCallback` with `fetch()` — server waits for all async operations before rendering.
|
|
15
|
+
- **RenderCore**: A headless, event-driven rendering class that follows the `wc-bindable` protocol for observable `html` / `loading` / `error` state.
|
|
16
|
+
- **Zero Browser Dependencies**: Runs in Node.js with happy-dom as the only runtime dependency.
|
|
17
|
+
|
|
18
|
+
### Unique Features
|
|
19
|
+
- **Drop-in SSR**: No changes to your client-side templates. Add `enable-ssr` to `<wcs-state>` and render with `renderToString()`.
|
|
20
|
+
- **Template Fragment Preservation**: `for`/`if` template sources are captured with UUID references so the client can re-execute structural directives.
|
|
21
|
+
- **Property Hydration**: DOM properties that can't be expressed as attributes (e.g., `innerHTML`) are serialized separately and restored during hydration.
|
|
22
|
+
- **wc-bindable Protocol**: `RenderCore` exposes rendering state via the standard protocol, enabling the same `bind()` pattern on both server and client.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @wcstack/server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### `renderToString()` — One-shot rendering
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import { renderToString } from "@wcstack/server";
|
|
36
|
+
|
|
37
|
+
const html = await renderToString(`
|
|
38
|
+
<wcs-state json='{"items":["Apple","Banana","Cherry"]}' enable-ssr>
|
|
39
|
+
</wcs-state>
|
|
40
|
+
<ul>
|
|
41
|
+
<template data-wcs="for: items">
|
|
42
|
+
<li data-wcs="textContent: items.*"></li>
|
|
43
|
+
</template>
|
|
44
|
+
</ul>
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
console.log(html);
|
|
48
|
+
// Fully rendered HTML with <wcs-ssr> hydration data
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `RenderCore` — Observable rendering with caching
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { RenderCore } from "@wcstack/server";
|
|
55
|
+
|
|
56
|
+
const renderer = new RenderCore();
|
|
57
|
+
|
|
58
|
+
// Listen to state changes via wc-bindable protocol
|
|
59
|
+
renderer.addEventListener("wcs-render:loading-changed", (e) => {
|
|
60
|
+
console.log("loading:", e.detail);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
renderer.addEventListener("wcs-render:html-changed", (e) => {
|
|
64
|
+
console.log("rendered:", e.detail.length, "bytes");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Render and cache
|
|
68
|
+
await renderer.render(templateHtml);
|
|
69
|
+
|
|
70
|
+
// Subsequent reads use the cached result
|
|
71
|
+
console.log(renderer.html);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
### `renderToString(html: string): Promise<string>`
|
|
77
|
+
|
|
78
|
+
Renders an HTML string containing `@wcstack/state` templates. Returns fully-rendered HTML with hydration data for any `<wcs-state enable-ssr>` elements.
|
|
79
|
+
|
|
80
|
+
**Rendering pipeline:**
|
|
81
|
+
1. Creates a happy-dom window and installs browser globals
|
|
82
|
+
2. Parses HTML and triggers `connectedCallback` on all `<wcs-state>` elements
|
|
83
|
+
3. Awaits all `$connectedCallback` promises (including `fetch()` calls)
|
|
84
|
+
4. Waits for `buildBindings` to complete
|
|
85
|
+
5. Generates `<wcs-ssr>` elements for states with `enable-ssr`
|
|
86
|
+
6. Restores globals and returns the rendered HTML
|
|
87
|
+
|
|
88
|
+
### `RenderCore`
|
|
89
|
+
|
|
90
|
+
Headless rendering class extending `EventTarget`. Implements the `wc-bindable` protocol.
|
|
91
|
+
|
|
92
|
+
| Property | Type | Description |
|
|
93
|
+
|----------|------|-------------|
|
|
94
|
+
| `html` | `string \| null` | Rendered HTML (cached after `render()`) |
|
|
95
|
+
| `loading` | `boolean` | `true` while rendering is in progress |
|
|
96
|
+
| `error` | `Error \| null` | Error from the last `render()` call, if any |
|
|
97
|
+
|
|
98
|
+
| Method | Returns | Description |
|
|
99
|
+
|--------|---------|-------------|
|
|
100
|
+
| `render(html)` | `Promise<string \| null>` | Renders the template and caches the result. Returns `null` on error. |
|
|
101
|
+
|
|
102
|
+
| Event | Detail | Description |
|
|
103
|
+
|-------|--------|-------------|
|
|
104
|
+
| `wcs-render:html-changed` | `string` | Fired when rendering completes successfully |
|
|
105
|
+
| `wcs-render:loading-changed` | `boolean` | Fired when loading state changes |
|
|
106
|
+
| `wcs-render:error` | `Error` | Fired when rendering fails |
|
|
107
|
+
|
|
108
|
+
**wc-bindable declaration:**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
static wcBindable = {
|
|
112
|
+
protocol: "wc-bindable",
|
|
113
|
+
version: 1,
|
|
114
|
+
properties: [
|
|
115
|
+
{ name: "html", event: "wcs-render:html-changed" },
|
|
116
|
+
{ name: "loading", event: "wcs-render:loading-changed" },
|
|
117
|
+
{ name: "error", event: "wcs-render:error" },
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Helper Functions
|
|
123
|
+
|
|
124
|
+
| Function | Description |
|
|
125
|
+
|----------|-------------|
|
|
126
|
+
| `installGlobals(window)` | Installs happy-dom globals on `globalThis`. Returns a restore function. |
|
|
127
|
+
| `extractStateData(stateEl)` | Extracts data properties from a `<wcs-state>` element (excludes `$`-prefixed keys and functions). |
|
|
128
|
+
|
|
129
|
+
### Constants
|
|
130
|
+
|
|
131
|
+
| Name | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `GLOBALS_KEYS` | Array of browser global keys installed during SSR (`document`, `HTMLElement`, `Node`, etc.) |
|
|
134
|
+
| `VERSION` | Package version string from `package.json` |
|
|
135
|
+
|
|
136
|
+
## SSR Output Structure
|
|
137
|
+
|
|
138
|
+
When a `<wcs-state>` has the `enable-ssr` attribute, `renderToString()` inserts a `<wcs-ssr>` element immediately before it containing all hydration data:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<!-- Generated by renderToString() -->
|
|
142
|
+
<wcs-ssr name="default" version="0.1.0">
|
|
143
|
+
|
|
144
|
+
<!-- State snapshot -->
|
|
145
|
+
<script type="application/json">{"items":["Apple","Banana","Cherry"]}</script>
|
|
146
|
+
|
|
147
|
+
<!-- Template fragments (for client-side re-execution) -->
|
|
148
|
+
<template id="uuid-1234" data-wcs="for: items">
|
|
149
|
+
<li data-wcs="textContent: items.*"></li>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<!-- Non-attribute properties (optional) -->
|
|
153
|
+
<script type="application/json" data-wcs-ssr-props>
|
|
154
|
+
{"wcs-ssr-0": {"innerHTML": "<b>rich</b>"}}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
</wcs-ssr>
|
|
158
|
+
|
|
159
|
+
<wcs-state json='...' enable-ssr></wcs-state>
|
|
160
|
+
|
|
161
|
+
<!-- Rendered output (visible immediately) -->
|
|
162
|
+
<ul>
|
|
163
|
+
<li>Apple</li>
|
|
164
|
+
<li>Banana</li>
|
|
165
|
+
<li>Cherry</li>
|
|
166
|
+
</ul>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The client-side `@wcstack/state` reads the `<wcs-ssr>` element during hydration, restores state and templates, and resumes reactivity without re-rendering.
|
|
170
|
+
|
|
171
|
+
## Server Integration Example
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import { createServer } from "node:http";
|
|
175
|
+
import { RenderCore } from "@wcstack/server";
|
|
176
|
+
|
|
177
|
+
const renderer = new RenderCore();
|
|
178
|
+
|
|
179
|
+
const template = `
|
|
180
|
+
<wcs-state enable-ssr>
|
|
181
|
+
<script type="module">
|
|
182
|
+
export default {
|
|
183
|
+
async $connectedCallback() {
|
|
184
|
+
const res = await fetch("http://localhost:3000/api/data");
|
|
185
|
+
this.items = await res.json();
|
|
186
|
+
},
|
|
187
|
+
items: []
|
|
188
|
+
};
|
|
189
|
+
</script>
|
|
190
|
+
</wcs-state>
|
|
191
|
+
<ul>
|
|
192
|
+
<template data-wcs="for: items">
|
|
193
|
+
<li data-wcs="textContent: items.*"></li>
|
|
194
|
+
</template>
|
|
195
|
+
</ul>
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
createServer(async (req, res) => {
|
|
199
|
+
if (!renderer.html) {
|
|
200
|
+
await renderer.render(template);
|
|
201
|
+
}
|
|
202
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
203
|
+
res.end(renderer.html);
|
|
204
|
+
}).listen(3000);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Input HTML Rules
|
|
208
|
+
|
|
209
|
+
- Pass only the contents of `<body>` — do not include `<html>`, `<head>`, or `<body>` tags.
|
|
210
|
+
- `<script>` / `<link>` external resource loading is not executed.
|
|
211
|
+
→ Provide required packages via `options.bootstraps`.
|
|
212
|
+
|
|
213
|
+
## What SSR Can Do
|
|
214
|
+
|
|
215
|
+
### State Initialization & Data Fetching
|
|
216
|
+
|
|
217
|
+
- Load `<wcs-state>` from `json` attribute, `src` attribute, or inline `<script type="module">`
|
|
218
|
+
- Execute `$connectedCallback` for server-side fetch (API calls, etc.)
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<!-- Direct JSON -->
|
|
222
|
+
<wcs-state enable-ssr json='{"title":"Hello"}'></wcs-state>
|
|
223
|
+
|
|
224
|
+
<!-- Fetch data from API in $connectedCallback -->
|
|
225
|
+
<!-- $connectedCallback is defined as a method on the state object; `this` is the state proxy -->
|
|
226
|
+
<wcs-state enable-ssr>
|
|
227
|
+
<script type="module">
|
|
228
|
+
export default {
|
|
229
|
+
async $connectedCallback() {
|
|
230
|
+
const res = await fetch('/api/users');
|
|
231
|
+
this.users = await res.json();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
</script>
|
|
235
|
+
</wcs-state>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Server Communication with wcs-fetch
|
|
239
|
+
|
|
240
|
+
- `<wcs-fetch>` auto-fetch (without `manual`) also executes on the server
|
|
241
|
+
- Use `manual` + `$connectedCallback` for explicit control:
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<wcs-fetch id="api" url="/api/users" manual></wcs-fetch>
|
|
245
|
+
<wcs-state enable-ssr>
|
|
246
|
+
<script type="module">
|
|
247
|
+
export default {
|
|
248
|
+
async $connectedCallback() {
|
|
249
|
+
const el = document.getElementById('api');
|
|
250
|
+
this.users = await el.fetch();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
</script>
|
|
254
|
+
</wcs-state>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> Note: `bootstrapFetch` must be included in the `bootstraps` option.
|
|
258
|
+
|
|
259
|
+
### Bindings & Structural Rendering
|
|
260
|
+
|
|
261
|
+
- `data-wcs` binding application (text, attribute, class, style, property)
|
|
262
|
+
- `<template data-wcs="for:">` / `if:` / `elseif:` / `else:` structural rendering
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<ul>
|
|
266
|
+
<template data-wcs="for: users">
|
|
267
|
+
<li data-wcs="textContent: .name"></li>
|
|
268
|
+
</template>
|
|
269
|
+
</ul>
|
|
270
|
+
<template data-wcs="if: isAdmin">
|
|
271
|
+
<div class="admin-panel">...</div>
|
|
272
|
+
</template>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Hydration
|
|
276
|
+
|
|
277
|
+
- Automatic `<wcs-ssr>` metadata generation for `<wcs-state enable-ssr>`
|
|
278
|
+
- Client-side hydration restores bindings without re-rendering
|
|
279
|
+
- `<wcs-state>` without `enable-ssr` runs client-only (partial CSR)
|
|
280
|
+
|
|
281
|
+
### Custom Element Waiting
|
|
282
|
+
|
|
283
|
+
- Automatically awaits all custom elements with `static hasConnectedCallbackPromise = true`
|
|
284
|
+
|
|
285
|
+
## What SSR Cannot Do
|
|
286
|
+
|
|
287
|
+
- Execute `<script src="...">` or `<link>` in `<head>`
|
|
288
|
+
- Access browser-specific APIs (localStorage, sessionStorage, navigator, etc.)
|
|
289
|
+
- Render Shadow DOM (Declarative Shadow DOM not supported)
|
|
290
|
+
- Register event handlers (restored via client-side hydration)
|
|
291
|
+
- Load components dynamically via `<wcs-autoloader>`
|
|
292
|
+
|
|
293
|
+
## HTML Splitting Pattern
|
|
294
|
+
|
|
295
|
+
`renderToString` receives only the `<body>` contents. Wrap the result with `<head>` and `<script>` tags on the outside:
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
// server.js
|
|
299
|
+
const ssrBody = await renderToString(template, {
|
|
300
|
+
baseUrl: 'http://localhost:3001',
|
|
301
|
+
});
|
|
302
|
+
const page = `<!DOCTYPE html>
|
|
303
|
+
<html lang="en">
|
|
304
|
+
<head>
|
|
305
|
+
<script type="module" src="/packages/state/dist/auto.js"></script>
|
|
306
|
+
</head>
|
|
307
|
+
<body>${ssrBody}</body>
|
|
308
|
+
</html>`;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Using Multiple Packages
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
import { bootstrapState, getBindingsReady } from '@wcstack/state';
|
|
315
|
+
import { bootstrapFetch } from '@wcstack/fetch';
|
|
316
|
+
|
|
317
|
+
const ssrBody = await renderToString(template, {
|
|
318
|
+
baseUrl: 'http://localhost:3001',
|
|
319
|
+
bootstraps: [bootstrapState, bootstrapFetch],
|
|
320
|
+
ready: [(doc) => getBindingsReady(doc)],
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## How It Works
|
|
325
|
+
|
|
326
|
+
### Rendering Pipeline
|
|
327
|
+
|
|
328
|
+
1. **Global Setup**: Creates a happy-dom `Window` and temporarily installs browser globals (`document`, `HTMLElement`, `MutationObserver`, etc.) on `globalThis`. Disables `URL.createObjectURL` to force the base64 data URL fallback for inline scripts.
|
|
329
|
+
|
|
330
|
+
2. **SSR Mode**: Sets `data-wcs-server` attribute on the `<html>` element. `@wcstack/state` detects this attribute to enable SSR behavior.
|
|
331
|
+
|
|
332
|
+
3. **Bootstrap**: Calls user-provided bootstrap functions (defaults to `bootstrapState()` if omitted).
|
|
333
|
+
|
|
334
|
+
4. **HTML Parse & Callback**: Sets `document.body.innerHTML`, which triggers happy-dom's element lifecycle. Each `<wcs-state>` loads its data source and runs `$connectedCallback`. Awaits all custom elements with `hasConnectedCallbackPromise`.
|
|
335
|
+
|
|
336
|
+
5. **Ready**: Awaits user-provided ready functions (defaults to `getBindingsReady()`) — text interpolation, attribute mapping, list expansion, conditional evaluation.
|
|
337
|
+
|
|
338
|
+
6. **SSR Metadata**: Each `<wcs-state enable-ssr>` automatically generates a `<wcs-ssr>` element in its `connectedCallback`.
|
|
339
|
+
|
|
340
|
+
7. **Cleanup**: Restores original globals and closes the happy-dom window.
|
|
341
|
+
|
|
342
|
+
### Client-Side Hydration
|
|
343
|
+
|
|
344
|
+
The client-side `@wcstack/state` detects `<wcs-ssr>` elements and:
|
|
345
|
+
1. Restores state from the JSON snapshot (skipping network requests)
|
|
346
|
+
2. Re-attaches template fragments using UUID references
|
|
347
|
+
3. Applies non-attribute properties from the props script
|
|
348
|
+
4. Resumes normal reactive binding
|
|
349
|
+
|
|
350
|
+
The rendered DOM is visible immediately — hydration only wires up interactivity.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Window } from 'happy-dom';
|
|
2
|
+
|
|
3
|
+
declare const GLOBALS_KEYS: string[];
|
|
4
|
+
declare function installGlobals(window: Window): () => void;
|
|
5
|
+
declare function installBaseUrl(baseUrl: string): () => void;
|
|
6
|
+
/** @deprecated Use Ssr.extractStateData() from @wcstack/state instead */
|
|
7
|
+
declare function extractStateData(stateEl: any): Record<string, any>;
|
|
8
|
+
type BootstrapFunction = () => void;
|
|
9
|
+
type ReadyFunction = (doc: Document) => Promise<void>;
|
|
10
|
+
interface RenderOptions {
|
|
11
|
+
/** 相対 URL を解決するベース URL (例: "http://localhost:3001") */
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
/** bootstrap 関数の配列。省略時は @wcstack/state を自動ロード */
|
|
14
|
+
bootstraps?: BootstrapFunction[];
|
|
15
|
+
/** バインディング等の非同期初期化完了を待機する関数の配列 */
|
|
16
|
+
ready?: ReadyFunction[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* HTML 文字列を SSR レンダリングして返す。
|
|
20
|
+
*
|
|
21
|
+
* ## 入力 HTML のルール
|
|
22
|
+
* - `<body>` の中身だけを渡す(`<html>`, `<head>`, `<body>` タグは含めない)
|
|
23
|
+
* - `<script>` / `<link>` による外部リソース読み込みは実行されない
|
|
24
|
+
* → 必要なパッケージは `options.bootstraps` で明示的に渡す
|
|
25
|
+
*
|
|
26
|
+
* ## SSR でできること
|
|
27
|
+
*
|
|
28
|
+
* ### 状態の初期化とデータ取得
|
|
29
|
+
* - `<wcs-state>` の状態ロード(json 属性, src 属性, inline `<script type="module">`)
|
|
30
|
+
* - `$connectedCallback` でのサーバーサイド fetch(API 呼び出し、DB 問い合わせ等)
|
|
31
|
+
*
|
|
32
|
+
* ```html
|
|
33
|
+
* <!-- JSON 直接指定 -->
|
|
34
|
+
* <wcs-state enable-ssr json='{"title":"Hello"}'></wcs-state>
|
|
35
|
+
*
|
|
36
|
+
* <!-- $connectedCallback で API からデータ取得 -->
|
|
37
|
+
* <!-- $connectedCallback は状態オブジェクトのメソッドとして定義し、this が state proxy -->
|
|
38
|
+
* <wcs-state enable-ssr>
|
|
39
|
+
* <script type="module">
|
|
40
|
+
* export default {
|
|
41
|
+
* async $connectedCallback() {
|
|
42
|
+
* const res = await fetch('/api/users');
|
|
43
|
+
* this.users = await res.json();
|
|
44
|
+
* }
|
|
45
|
+
* };
|
|
46
|
+
* </script>
|
|
47
|
+
* </wcs-state>
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ### wcs-fetch を使ったサーバー通信
|
|
51
|
+
* - `<wcs-fetch>` の auto-fetch(`manual` なし)はサーバーでも実行される
|
|
52
|
+
* - `manual` + `$connectedCallback` で明示的に制御する場合:
|
|
53
|
+
*
|
|
54
|
+
* ```html
|
|
55
|
+
* <wcs-fetch id="api" url="/api/users" manual></wcs-fetch>
|
|
56
|
+
* <wcs-state enable-ssr>
|
|
57
|
+
* <script type="module">
|
|
58
|
+
* export default {
|
|
59
|
+
* async $connectedCallback() {
|
|
60
|
+
* const el = document.getElementById('api');
|
|
61
|
+
* this.users = await el.fetch();
|
|
62
|
+
* }
|
|
63
|
+
* };
|
|
64
|
+
* </script>
|
|
65
|
+
* </wcs-state>
|
|
66
|
+
* ```
|
|
67
|
+
* ※ `bootstraps` に `bootstrapFetch` を含める必要あり
|
|
68
|
+
*
|
|
69
|
+
* ### バインディングと構造レンダリング
|
|
70
|
+
* - `data-wcs` バインディングの適用(text, attribute, class, style, property)
|
|
71
|
+
* - `<template data-wcs="for:">` / `if:` / `elseif:` / `else:` の構造レンダリング
|
|
72
|
+
*
|
|
73
|
+
* ```html
|
|
74
|
+
* <ul>
|
|
75
|
+
* <template data-wcs="for: users">
|
|
76
|
+
* <li data-wcs="textContent: .name"></li>
|
|
77
|
+
* </template>
|
|
78
|
+
* </ul>
|
|
79
|
+
* <template data-wcs="if: isAdmin">
|
|
80
|
+
* <div class="admin-panel">...</div>
|
|
81
|
+
* </template>
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ### ハイドレーション
|
|
85
|
+
* - `enable-ssr` 付き `<wcs-state>` の `<wcs-ssr>` メタデータ自動生成
|
|
86
|
+
* - クライアント側でのハイドレーション(再レンダリングなしでバインディング復元)
|
|
87
|
+
* - `enable-ssr` を外した `<wcs-state>` はクライアントのみで動作(部分 CSR)
|
|
88
|
+
*
|
|
89
|
+
* ### カスタム要素の待機
|
|
90
|
+
* - `static hasConnectedCallbackPromise = true` プロトコル準拠の全カスタム要素を自動待機
|
|
91
|
+
*
|
|
92
|
+
* ## SSR でできないこと
|
|
93
|
+
* - `<head>` 内の `<script src="...">` や `<link>` の自動実行
|
|
94
|
+
* - ブラウザ固有 API(localStorage, sessionStorage, navigator 等)
|
|
95
|
+
* - Shadow DOM のレンダリング(Declarative Shadow DOM 非対応)
|
|
96
|
+
* - イベントハンドラの登録(クライアント側のハイドレーションで復元)
|
|
97
|
+
* - `<wcs-autoloader>` による動的コンポーネント読み込み
|
|
98
|
+
*
|
|
99
|
+
* ## HTML の分割パターン
|
|
100
|
+
* ```
|
|
101
|
+
* // server.js
|
|
102
|
+
* const ssrBody = await renderToString(template, { ... });
|
|
103
|
+
* const page = `<!DOCTYPE html>
|
|
104
|
+
* <html lang="ja">
|
|
105
|
+
* <head>
|
|
106
|
+
* <script type="module" src="/packages/state/dist/auto.js"></script>
|
|
107
|
+
* </head>
|
|
108
|
+
* <body>${ssrBody}</body>
|
|
109
|
+
* </html>`;
|
|
110
|
+
* ```
|
|
111
|
+
* `renderToString` には `<body>` の中身だけを渡し、
|
|
112
|
+
* `<head>` や `<script>` タグは外側のテンプレートで囲む。
|
|
113
|
+
*/
|
|
114
|
+
declare function renderToString(html: string, options?: RenderOptions): Promise<string>;
|
|
115
|
+
|
|
116
|
+
declare const VERSION: string;
|
|
117
|
+
|
|
118
|
+
interface IWcBindableProperty {
|
|
119
|
+
readonly name: string;
|
|
120
|
+
readonly event: string;
|
|
121
|
+
readonly getter?: (event: Event) => any;
|
|
122
|
+
}
|
|
123
|
+
interface IWcBindable {
|
|
124
|
+
readonly protocol: "wc-bindable";
|
|
125
|
+
readonly version: number;
|
|
126
|
+
readonly properties: IWcBindableProperty[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Value types for RenderCore (headless) — the 3 async state properties.
|
|
130
|
+
* Use with `bind()` from `@wc-bindable/core` for compile-time type checking.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const core = new RenderCore();
|
|
135
|
+
* bind(core, (name: keyof WcsRenderValues, value) => { ... });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
interface WcsRenderValues {
|
|
139
|
+
html: string | null;
|
|
140
|
+
loading: boolean;
|
|
141
|
+
error: Error | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
declare class RenderCore extends EventTarget {
|
|
145
|
+
static wcBindable: IWcBindable;
|
|
146
|
+
private _html;
|
|
147
|
+
private _loading;
|
|
148
|
+
private _error;
|
|
149
|
+
get html(): string | null;
|
|
150
|
+
get loading(): boolean;
|
|
151
|
+
get error(): Error | null;
|
|
152
|
+
private _setLoading;
|
|
153
|
+
private _setHtml;
|
|
154
|
+
private _setError;
|
|
155
|
+
render(html: string): Promise<string | null>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { GLOBALS_KEYS, RenderCore, VERSION, extractStateData, installBaseUrl, installGlobals, renderToString };
|
|
159
|
+
export type { BootstrapFunction, IWcBindable, IWcBindableProperty, ReadyFunction, RenderOptions, WcsRenderValues };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Window } from 'happy-dom';
|
|
2
|
+
|
|
3
|
+
const GLOBALS_KEYS = [
|
|
4
|
+
'document', 'customElements', 'HTMLElement',
|
|
5
|
+
'DocumentFragment', 'Node', 'NodeFilter', 'Comment', 'Text',
|
|
6
|
+
'MutationObserver', 'ShadowRoot', 'Element', 'HTMLTemplateElement',
|
|
7
|
+
];
|
|
8
|
+
function installGlobals(window) {
|
|
9
|
+
const saved = {};
|
|
10
|
+
for (const key of GLOBALS_KEYS) {
|
|
11
|
+
saved[key] = globalThis[key];
|
|
12
|
+
globalThis[key] = window[key];
|
|
13
|
+
}
|
|
14
|
+
// URL.createObjectURL を無効化して、
|
|
15
|
+
// loadFromInnerScript が base64 data: URL フォールバックを使うようにする
|
|
16
|
+
const origCreateObjectURL = URL.createObjectURL;
|
|
17
|
+
URL.createObjectURL = undefined;
|
|
18
|
+
return () => {
|
|
19
|
+
URL.createObjectURL = origCreateObjectURL;
|
|
20
|
+
for (const key of GLOBALS_KEYS) {
|
|
21
|
+
globalThis[key] = saved[key];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function installBaseUrl(baseUrl) {
|
|
26
|
+
const OrigURL = globalThis.URL;
|
|
27
|
+
const base = baseUrl;
|
|
28
|
+
globalThis.URL = class extends OrigURL {
|
|
29
|
+
constructor(input, inputBase) {
|
|
30
|
+
if (typeof input === 'string' && input.startsWith('/') && inputBase === undefined) {
|
|
31
|
+
super(input, base);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
super(input, inputBase);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// 静的メソッドを引き継ぐ
|
|
39
|
+
globalThis.URL.createObjectURL = OrigURL.createObjectURL;
|
|
40
|
+
globalThis.URL.revokeObjectURL = OrigURL.revokeObjectURL;
|
|
41
|
+
return () => { globalThis.URL = OrigURL; };
|
|
42
|
+
}
|
|
43
|
+
/** @deprecated Use Ssr.extractStateData() from @wcstack/state instead */
|
|
44
|
+
function extractStateData(stateEl) {
|
|
45
|
+
const raw = stateEl.__state;
|
|
46
|
+
if (!raw || typeof raw !== 'object')
|
|
47
|
+
return {};
|
|
48
|
+
const data = {};
|
|
49
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
50
|
+
if (!key.startsWith('$') && typeof value !== 'function') {
|
|
51
|
+
data[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
async function loadDefaultBootstraps() {
|
|
57
|
+
const { bootstrapState, getBindingsReady } = await import('@wcstack/state');
|
|
58
|
+
return {
|
|
59
|
+
bootstraps: [bootstrapState],
|
|
60
|
+
ready: [(doc) => getBindingsReady(doc)],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* HTML 文字列を SSR レンダリングして返す。
|
|
65
|
+
*
|
|
66
|
+
* ## 入力 HTML のルール
|
|
67
|
+
* - `<body>` の中身だけを渡す(`<html>`, `<head>`, `<body>` タグは含めない)
|
|
68
|
+
* - `<script>` / `<link>` による外部リソース読み込みは実行されない
|
|
69
|
+
* → 必要なパッケージは `options.bootstraps` で明示的に渡す
|
|
70
|
+
*
|
|
71
|
+
* ## SSR でできること
|
|
72
|
+
*
|
|
73
|
+
* ### 状態の初期化とデータ取得
|
|
74
|
+
* - `<wcs-state>` の状態ロード(json 属性, src 属性, inline `<script type="module">`)
|
|
75
|
+
* - `$connectedCallback` でのサーバーサイド fetch(API 呼び出し、DB 問い合わせ等)
|
|
76
|
+
*
|
|
77
|
+
* ```html
|
|
78
|
+
* <!-- JSON 直接指定 -->
|
|
79
|
+
* <wcs-state enable-ssr json='{"title":"Hello"}'></wcs-state>
|
|
80
|
+
*
|
|
81
|
+
* <!-- $connectedCallback で API からデータ取得 -->
|
|
82
|
+
* <!-- $connectedCallback は状態オブジェクトのメソッドとして定義し、this が state proxy -->
|
|
83
|
+
* <wcs-state enable-ssr>
|
|
84
|
+
* <script type="module">
|
|
85
|
+
* export default {
|
|
86
|
+
* async $connectedCallback() {
|
|
87
|
+
* const res = await fetch('/api/users');
|
|
88
|
+
* this.users = await res.json();
|
|
89
|
+
* }
|
|
90
|
+
* };
|
|
91
|
+
* </script>
|
|
92
|
+
* </wcs-state>
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* ### wcs-fetch を使ったサーバー通信
|
|
96
|
+
* - `<wcs-fetch>` の auto-fetch(`manual` なし)はサーバーでも実行される
|
|
97
|
+
* - `manual` + `$connectedCallback` で明示的に制御する場合:
|
|
98
|
+
*
|
|
99
|
+
* ```html
|
|
100
|
+
* <wcs-fetch id="api" url="/api/users" manual></wcs-fetch>
|
|
101
|
+
* <wcs-state enable-ssr>
|
|
102
|
+
* <script type="module">
|
|
103
|
+
* export default {
|
|
104
|
+
* async $connectedCallback() {
|
|
105
|
+
* const el = document.getElementById('api');
|
|
106
|
+
* this.users = await el.fetch();
|
|
107
|
+
* }
|
|
108
|
+
* };
|
|
109
|
+
* </script>
|
|
110
|
+
* </wcs-state>
|
|
111
|
+
* ```
|
|
112
|
+
* ※ `bootstraps` に `bootstrapFetch` を含める必要あり
|
|
113
|
+
*
|
|
114
|
+
* ### バインディングと構造レンダリング
|
|
115
|
+
* - `data-wcs` バインディングの適用(text, attribute, class, style, property)
|
|
116
|
+
* - `<template data-wcs="for:">` / `if:` / `elseif:` / `else:` の構造レンダリング
|
|
117
|
+
*
|
|
118
|
+
* ```html
|
|
119
|
+
* <ul>
|
|
120
|
+
* <template data-wcs="for: users">
|
|
121
|
+
* <li data-wcs="textContent: .name"></li>
|
|
122
|
+
* </template>
|
|
123
|
+
* </ul>
|
|
124
|
+
* <template data-wcs="if: isAdmin">
|
|
125
|
+
* <div class="admin-panel">...</div>
|
|
126
|
+
* </template>
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* ### ハイドレーション
|
|
130
|
+
* - `enable-ssr` 付き `<wcs-state>` の `<wcs-ssr>` メタデータ自動生成
|
|
131
|
+
* - クライアント側でのハイドレーション(再レンダリングなしでバインディング復元)
|
|
132
|
+
* - `enable-ssr` を外した `<wcs-state>` はクライアントのみで動作(部分 CSR)
|
|
133
|
+
*
|
|
134
|
+
* ### カスタム要素の待機
|
|
135
|
+
* - `static hasConnectedCallbackPromise = true` プロトコル準拠の全カスタム要素を自動待機
|
|
136
|
+
*
|
|
137
|
+
* ## SSR でできないこと
|
|
138
|
+
* - `<head>` 内の `<script src="...">` や `<link>` の自動実行
|
|
139
|
+
* - ブラウザ固有 API(localStorage, sessionStorage, navigator 等)
|
|
140
|
+
* - Shadow DOM のレンダリング(Declarative Shadow DOM 非対応)
|
|
141
|
+
* - イベントハンドラの登録(クライアント側のハイドレーションで復元)
|
|
142
|
+
* - `<wcs-autoloader>` による動的コンポーネント読み込み
|
|
143
|
+
*
|
|
144
|
+
* ## HTML の分割パターン
|
|
145
|
+
* ```
|
|
146
|
+
* // server.js
|
|
147
|
+
* const ssrBody = await renderToString(template, { ... });
|
|
148
|
+
* const page = `<!DOCTYPE html>
|
|
149
|
+
* <html lang="ja">
|
|
150
|
+
* <head>
|
|
151
|
+
* <script type="module" src="/packages/state/dist/auto.js"></script>
|
|
152
|
+
* </head>
|
|
153
|
+
* <body>${ssrBody}</body>
|
|
154
|
+
* </html>`;
|
|
155
|
+
* ```
|
|
156
|
+
* `renderToString` には `<body>` の中身だけを渡し、
|
|
157
|
+
* `<head>` や `<script>` タグは外側のテンプレートで囲む。
|
|
158
|
+
*/
|
|
159
|
+
async function renderToString(html, options) {
|
|
160
|
+
const window = new Window();
|
|
161
|
+
const restoreGlobals = installGlobals(window);
|
|
162
|
+
const document = window.document;
|
|
163
|
+
// 相対 URL を baseUrl で解決する URL コンストラクタパッチをインストール
|
|
164
|
+
const restoreBaseUrl = options?.baseUrl
|
|
165
|
+
? installBaseUrl(options.baseUrl)
|
|
166
|
+
: null;
|
|
167
|
+
// bootstrap / ready の解決
|
|
168
|
+
const hasCustomBootstraps = options?.bootstraps !== undefined;
|
|
169
|
+
const defaults = hasCustomBootstraps ? null : await loadDefaultBootstraps();
|
|
170
|
+
const bootstraps = options?.bootstraps ?? defaults.bootstraps;
|
|
171
|
+
const readyFns = options?.ready ?? defaults?.ready ?? [];
|
|
172
|
+
for (const bootstrap of bootstraps) {
|
|
173
|
+
bootstrap();
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// SSR モードを html 要素に設定
|
|
177
|
+
document.documentElement.setAttribute('data-wcs-server', '');
|
|
178
|
+
// HTML をパース
|
|
179
|
+
// connectedCallback が自動発火 → state ロード → $connectedCallback 実行
|
|
180
|
+
document.body.innerHTML = html;
|
|
181
|
+
// connectedCallbackPromise プロトコル準拠の全カスタム要素の完了を待機
|
|
182
|
+
const promises = [];
|
|
183
|
+
for (const el of document.querySelectorAll('*-*')) {
|
|
184
|
+
const ctor = el.constructor;
|
|
185
|
+
if (ctor.hasConnectedCallbackPromise) {
|
|
186
|
+
promises.push(el.connectedCallbackPromise);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await Promise.all(promises);
|
|
190
|
+
// 非同期初期化の完了を待機
|
|
191
|
+
await Promise.all(readyFns.map(fn => fn(document)));
|
|
192
|
+
return document.body.innerHTML;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
restoreBaseUrl?.();
|
|
196
|
+
restoreGlobals();
|
|
197
|
+
await window.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var version = "0.2.3";
|
|
202
|
+
var pkg = {
|
|
203
|
+
version: version};
|
|
204
|
+
|
|
205
|
+
const VERSION = pkg.version;
|
|
206
|
+
|
|
207
|
+
class RenderCore extends EventTarget {
|
|
208
|
+
static wcBindable = {
|
|
209
|
+
protocol: "wc-bindable",
|
|
210
|
+
version: 1,
|
|
211
|
+
properties: [
|
|
212
|
+
{ name: "html", event: "wcs-render:html-changed" },
|
|
213
|
+
{ name: "loading", event: "wcs-render:loading-changed" },
|
|
214
|
+
{ name: "error", event: "wcs-render:error" },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
_html = null;
|
|
218
|
+
_loading = false;
|
|
219
|
+
_error = null;
|
|
220
|
+
get html() {
|
|
221
|
+
return this._html;
|
|
222
|
+
}
|
|
223
|
+
get loading() {
|
|
224
|
+
return this._loading;
|
|
225
|
+
}
|
|
226
|
+
get error() {
|
|
227
|
+
return this._error;
|
|
228
|
+
}
|
|
229
|
+
_setLoading(loading) {
|
|
230
|
+
this._loading = loading;
|
|
231
|
+
this.dispatchEvent(new CustomEvent("wcs-render:loading-changed", {
|
|
232
|
+
detail: loading,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
_setHtml(html) {
|
|
236
|
+
this._html = html;
|
|
237
|
+
this.dispatchEvent(new CustomEvent("wcs-render:html-changed", {
|
|
238
|
+
detail: html,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
_setError(error) {
|
|
242
|
+
this._error = error;
|
|
243
|
+
this.dispatchEvent(new CustomEvent("wcs-render:error", {
|
|
244
|
+
detail: error,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
async render(html) {
|
|
248
|
+
this._setLoading(true);
|
|
249
|
+
this._error = null;
|
|
250
|
+
try {
|
|
251
|
+
const result = await renderToString(html);
|
|
252
|
+
this._setHtml(result);
|
|
253
|
+
this._setLoading(false);
|
|
254
|
+
return this._html;
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
this._setError(e instanceof Error ? e : new Error(String(e)));
|
|
258
|
+
this._setLoading(false);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export { GLOBALS_KEYS, RenderCore, VERSION, extractStateData, installBaseUrl, installGlobals, renderToString };
|
|
265
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../../src/render.ts","../../src/version.ts","../../src/RenderCore.ts"],"sourcesContent":["import { Window } from 'happy-dom';\r\n\r\nexport const GLOBALS_KEYS = [\r\n 'document', 'customElements', 'HTMLElement',\r\n 'DocumentFragment', 'Node', 'NodeFilter', 'Comment', 'Text',\r\n 'MutationObserver', 'ShadowRoot', 'Element', 'HTMLTemplateElement',\r\n];\r\n\r\nexport function installGlobals(window: Window): () => void {\r\n const saved: Record<string, any> = {};\r\n for (const key of GLOBALS_KEYS) {\r\n saved[key] = (globalThis as any)[key];\r\n (globalThis as any)[key] = (window as any)[key];\r\n }\r\n // URL.createObjectURL を無効化して、\r\n // loadFromInnerScript が base64 data: URL フォールバックを使うようにする\r\n const origCreateObjectURL = URL.createObjectURL;\r\n (URL as any).createObjectURL = undefined;\r\n return () => {\r\n URL.createObjectURL = origCreateObjectURL;\r\n for (const key of GLOBALS_KEYS) {\r\n (globalThis as any)[key] = saved[key];\r\n }\r\n };\r\n}\r\n\r\nexport function installBaseUrl(baseUrl: string): () => void {\r\n const OrigURL = globalThis.URL;\r\n const base = baseUrl;\r\n globalThis.URL = class extends OrigURL {\r\n constructor(input: string | URL, inputBase?: string | URL) {\r\n if (typeof input === 'string' && input.startsWith('/') && inputBase === undefined) {\r\n super(input, base);\r\n } else {\r\n super(input as string, inputBase);\r\n }\r\n }\r\n } as typeof URL;\r\n // 静的メソッドを引き継ぐ\r\n globalThis.URL.createObjectURL = OrigURL.createObjectURL;\r\n globalThis.URL.revokeObjectURL = OrigURL.revokeObjectURL;\r\n return () => { globalThis.URL = OrigURL; };\r\n}\r\n\r\n/** @deprecated Use Ssr.extractStateData() from @wcstack/state instead */\r\nexport function extractStateData(stateEl: any): Record<string, any> {\r\n const raw = (stateEl as any).__state;\r\n if (!raw || typeof raw !== 'object') return {};\r\n const data: Record<string, any> = {};\r\n for (const [key, value] of Object.entries(raw)) {\r\n if (!key.startsWith('$') && typeof value !== 'function') {\r\n data[key] = value;\r\n }\r\n }\r\n return data;\r\n}\r\n\r\nexport type BootstrapFunction = () => void;\r\nexport type ReadyFunction = (doc: Document) => Promise<void>;\r\n\r\nexport interface RenderOptions {\r\n /** 相対 URL を解決するベース URL (例: \"http://localhost:3001\") */\r\n baseUrl?: string;\r\n /** bootstrap 関数の配列。省略時は @wcstack/state を自動ロード */\r\n bootstraps?: BootstrapFunction[];\r\n /** バインディング等の非同期初期化完了を待機する関数の配列 */\r\n ready?: ReadyFunction[];\r\n}\r\n\r\nasync function loadDefaultBootstraps(): Promise<{\r\n bootstraps: BootstrapFunction[];\r\n ready: ReadyFunction[];\r\n}> {\r\n const { bootstrapState, getBindingsReady } = await import('@wcstack/state');\r\n return {\r\n bootstraps: [bootstrapState],\r\n ready: [(doc: Document) => getBindingsReady(doc as any)],\r\n };\r\n}\r\n\r\n/**\r\n * HTML 文字列を SSR レンダリングして返す。\r\n *\r\n * ## 入力 HTML のルール\r\n * - `<body>` の中身だけを渡す(`<html>`, `<head>`, `<body>` タグは含めない)\r\n * - `<script>` / `<link>` による外部リソース読み込みは実行されない\r\n * → 必要なパッケージは `options.bootstraps` で明示的に渡す\r\n *\r\n * ## SSR でできること\r\n *\r\n * ### 状態の初期化とデータ取得\r\n * - `<wcs-state>` の状態ロード(json 属性, src 属性, inline `<script type=\"module\">`)\r\n * - `$connectedCallback` でのサーバーサイド fetch(API 呼び出し、DB 問い合わせ等)\r\n *\r\n * ```html\r\n * <!-- JSON 直接指定 -->\r\n * <wcs-state enable-ssr json='{\"title\":\"Hello\"}'></wcs-state>\r\n *\r\n * <!-- $connectedCallback で API からデータ取得 -->\r\n * <!-- $connectedCallback は状態オブジェクトのメソッドとして定義し、this が state proxy -->\r\n * <wcs-state enable-ssr>\r\n * <script type=\"module\">\r\n * export default {\r\n * async $connectedCallback() {\r\n * const res = await fetch('/api/users');\r\n * this.users = await res.json();\r\n * }\r\n * };\r\n * </script>\r\n * </wcs-state>\r\n * ```\r\n *\r\n * ### wcs-fetch を使ったサーバー通信\r\n * - `<wcs-fetch>` の auto-fetch(`manual` なし)はサーバーでも実行される\r\n * - `manual` + `$connectedCallback` で明示的に制御する場合:\r\n *\r\n * ```html\r\n * <wcs-fetch id=\"api\" url=\"/api/users\" manual></wcs-fetch>\r\n * <wcs-state enable-ssr>\r\n * <script type=\"module\">\r\n * export default {\r\n * async $connectedCallback() {\r\n * const el = document.getElementById('api');\r\n * this.users = await el.fetch();\r\n * }\r\n * };\r\n * </script>\r\n * </wcs-state>\r\n * ```\r\n * ※ `bootstraps` に `bootstrapFetch` を含める必要あり\r\n *\r\n * ### バインディングと構造レンダリング\r\n * - `data-wcs` バインディングの適用(text, attribute, class, style, property)\r\n * - `<template data-wcs=\"for:\">` / `if:` / `elseif:` / `else:` の構造レンダリング\r\n *\r\n * ```html\r\n * <ul>\r\n * <template data-wcs=\"for: users\">\r\n * <li data-wcs=\"textContent: .name\"></li>\r\n * </template>\r\n * </ul>\r\n * <template data-wcs=\"if: isAdmin\">\r\n * <div class=\"admin-panel\">...</div>\r\n * </template>\r\n * ```\r\n *\r\n * ### ハイドレーション\r\n * - `enable-ssr` 付き `<wcs-state>` の `<wcs-ssr>` メタデータ自動生成\r\n * - クライアント側でのハイドレーション(再レンダリングなしでバインディング復元)\r\n * - `enable-ssr` を外した `<wcs-state>` はクライアントのみで動作(部分 CSR)\r\n *\r\n * ### カスタム要素の待機\r\n * - `static hasConnectedCallbackPromise = true` プロトコル準拠の全カスタム要素を自動待機\r\n *\r\n * ## SSR でできないこと\r\n * - `<head>` 内の `<script src=\"...\">` や `<link>` の自動実行\r\n * - ブラウザ固有 API(localStorage, sessionStorage, navigator 等)\r\n * - Shadow DOM のレンダリング(Declarative Shadow DOM 非対応)\r\n * - イベントハンドラの登録(クライアント側のハイドレーションで復元)\r\n * - `<wcs-autoloader>` による動的コンポーネント読み込み\r\n *\r\n * ## HTML の分割パターン\r\n * ```\r\n * // server.js\r\n * const ssrBody = await renderToString(template, { ... });\r\n * const page = `<!DOCTYPE html>\r\n * <html lang=\"ja\">\r\n * <head>\r\n * <script type=\"module\" src=\"/packages/state/dist/auto.js\"></script>\r\n * </head>\r\n * <body>${ssrBody}</body>\r\n * </html>`;\r\n * ```\r\n * `renderToString` には `<body>` の中身だけを渡し、\r\n * `<head>` や `<script>` タグは外側のテンプレートで囲む。\r\n */\r\nexport async function renderToString(html: string, options?: RenderOptions): Promise<string> {\r\n const window = new Window();\r\n const restoreGlobals = installGlobals(window);\r\n const document = window.document;\r\n\r\n // 相対 URL を baseUrl で解決する URL コンストラクタパッチをインストール\r\n const restoreBaseUrl = options?.baseUrl\r\n ? installBaseUrl(options.baseUrl)\r\n : null;\r\n\r\n // bootstrap / ready の解決\r\n const hasCustomBootstraps = options?.bootstraps !== undefined;\r\n const defaults = hasCustomBootstraps ? null : await loadDefaultBootstraps();\r\n const bootstraps = options?.bootstraps ?? defaults!.bootstraps;\r\n const readyFns = options?.ready ?? defaults?.ready ?? [];\r\n\r\n for (const bootstrap of bootstraps) {\r\n bootstrap();\r\n }\r\n\r\n try {\r\n\r\n // SSR モードを html 要素に設定\r\n document.documentElement.setAttribute('data-wcs-server', '');\r\n\r\n // HTML をパース\r\n // connectedCallback が自動発火 → state ロード → $connectedCallback 実行\r\n document.body.innerHTML = html;\r\n\r\n // connectedCallbackPromise プロトコル準拠の全カスタム要素の完了を待機\r\n const promises: Promise<void>[] = [];\r\n for (const el of document.querySelectorAll('*-*')) {\r\n const ctor = el.constructor as any;\r\n if (ctor.hasConnectedCallbackPromise) {\r\n promises.push((el as any).connectedCallbackPromise);\r\n }\r\n }\r\n await Promise.all(promises);\r\n\r\n // 非同期初期化の完了を待機\r\n await Promise.all(readyFns.map(fn => fn(document as any)));\r\n\r\n return document.body.innerHTML;\r\n } finally {\r\n restoreBaseUrl?.();\r\n restoreGlobals();\r\n await window.close();\r\n }\r\n}\r\n","import pkg from \"../package.json\" with { type: \"json\" };\r\nexport const VERSION = pkg.version;\r\n","import { renderToString } from \"./render.js\";\r\nimport { IWcBindable } from \"./types.js\";\r\n\r\nexport class RenderCore extends EventTarget {\r\n static wcBindable: IWcBindable = {\r\n protocol: \"wc-bindable\",\r\n version: 1,\r\n properties: [\r\n { name: \"html\", event: \"wcs-render:html-changed\" },\r\n { name: \"loading\", event: \"wcs-render:loading-changed\" },\r\n { name: \"error\", event: \"wcs-render:error\" },\r\n ],\r\n };\r\n\r\n private _html: string | null = null;\r\n private _loading: boolean = false;\r\n private _error: Error | null = null;\r\n\r\n get html(): string | null {\r\n return this._html;\r\n }\r\n\r\n get loading(): boolean {\r\n return this._loading;\r\n }\r\n\r\n get error(): Error | null {\r\n return this._error;\r\n }\r\n\r\n private _setLoading(loading: boolean): void {\r\n this._loading = loading;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:loading-changed\", {\r\n detail: loading,\r\n }));\r\n }\r\n\r\n private _setHtml(html: string): void {\r\n this._html = html;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:html-changed\", {\r\n detail: html,\r\n }));\r\n }\r\n\r\n private _setError(error: Error): void {\r\n this._error = error;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:error\", {\r\n detail: error,\r\n }));\r\n }\r\n\r\n async render(html: string): Promise<string | null> {\r\n this._setLoading(true);\r\n this._error = null;\r\n\r\n try {\r\n const result = await renderToString(html);\r\n this._setHtml(result);\r\n this._setLoading(false);\r\n return this._html;\r\n } catch (e: any) {\r\n this._setError(e instanceof Error ? e : new Error(String(e)));\r\n this._setLoading(false);\r\n return null;\r\n }\r\n }\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Window as t}from"happy-dom";const e=["document","customElements","HTMLElement","DocumentFragment","Node","NodeFilter","Comment","Text","MutationObserver","ShadowRoot","Element","HTMLTemplateElement"];function r(t){const r={};for(const n of e)r[n]=globalThis[n],globalThis[n]=t[n];const n=URL.createObjectURL;return URL.createObjectURL=void 0,()=>{URL.createObjectURL=n;for(const t of e)globalThis[t]=r[t]}}function n(t){const e=globalThis.URL,r=t;return globalThis.URL=class extends e{constructor(t,e){"string"==typeof t&&t.startsWith("/")&&void 0===e?super(t,r):super(t,e)}},globalThis.URL.createObjectURL=e.createObjectURL,globalThis.URL.revokeObjectURL=e.revokeObjectURL,()=>{globalThis.URL=e}}function o(t){const e=t.__state;if(!e||"object"!=typeof e)return{};const r={};for(const[t,n]of Object.entries(e))t.startsWith("$")||"function"==typeof n||(r[t]=n);return r}async function s(e,o){const s=new t,a=r(s),i=s.document,c=o?.baseUrl?n(o.baseUrl):null,l=void 0!==o?.bootstraps?null:await async function(){const{bootstrapState:t,getBindingsReady:e}=await import("@wcstack/state");return{bootstraps:[t],ready:[t=>e(t)]}}(),d=o?.bootstraps??l.bootstraps,h=o?.ready??l?.ready??[];for(const t of d)t();try{i.documentElement.setAttribute("data-wcs-server",""),i.body.innerHTML=e;const t=[];for(const e of i.querySelectorAll("*-*")){e.constructor.hasConnectedCallbackPromise&&t.push(e.connectedCallbackPromise)}return await Promise.all(t),await Promise.all(h.map(t=>t(i))),i.body.innerHTML}finally{c?.(),a(),await s.close()}}const a="0.2.3";class i extends EventTarget{static wcBindable={protocol:"wc-bindable",version:1,properties:[{name:"html",event:"wcs-render:html-changed"},{name:"loading",event:"wcs-render:loading-changed"},{name:"error",event:"wcs-render:error"}]};_html=null;_loading=!1;_error=null;get html(){return this._html}get loading(){return this._loading}get error(){return this._error}_setLoading(t){this._loading=t,this.dispatchEvent(new CustomEvent("wcs-render:loading-changed",{detail:t}))}_setHtml(t){this._html=t,this.dispatchEvent(new CustomEvent("wcs-render:html-changed",{detail:t}))}_setError(t){this._error=t,this.dispatchEvent(new CustomEvent("wcs-render:error",{detail:t}))}async render(t){this._setLoading(!0),this._error=null;try{const e=await s(t);return this._setHtml(e),this._setLoading(!1),this._html}catch(t){return this._setError(t instanceof Error?t:new Error(String(t))),this._setLoading(!1),null}}}export{e as GLOBALS_KEYS,i as RenderCore,a as VERSION,o as extractStateData,n as installBaseUrl,r as installGlobals,s as renderToString};
|
|
2
|
+
//# sourceMappingURL=index.esm.min.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.min.js","sources":["../../src/render.ts","../../src/version.ts","../../src/RenderCore.ts"],"sourcesContent":["import { Window } from 'happy-dom';\r\n\r\nexport const GLOBALS_KEYS = [\r\n 'document', 'customElements', 'HTMLElement',\r\n 'DocumentFragment', 'Node', 'NodeFilter', 'Comment', 'Text',\r\n 'MutationObserver', 'ShadowRoot', 'Element', 'HTMLTemplateElement',\r\n];\r\n\r\nexport function installGlobals(window: Window): () => void {\r\n const saved: Record<string, any> = {};\r\n for (const key of GLOBALS_KEYS) {\r\n saved[key] = (globalThis as any)[key];\r\n (globalThis as any)[key] = (window as any)[key];\r\n }\r\n // URL.createObjectURL を無効化して、\r\n // loadFromInnerScript が base64 data: URL フォールバックを使うようにする\r\n const origCreateObjectURL = URL.createObjectURL;\r\n (URL as any).createObjectURL = undefined;\r\n return () => {\r\n URL.createObjectURL = origCreateObjectURL;\r\n for (const key of GLOBALS_KEYS) {\r\n (globalThis as any)[key] = saved[key];\r\n }\r\n };\r\n}\r\n\r\nexport function installBaseUrl(baseUrl: string): () => void {\r\n const OrigURL = globalThis.URL;\r\n const base = baseUrl;\r\n globalThis.URL = class extends OrigURL {\r\n constructor(input: string | URL, inputBase?: string | URL) {\r\n if (typeof input === 'string' && input.startsWith('/') && inputBase === undefined) {\r\n super(input, base);\r\n } else {\r\n super(input as string, inputBase);\r\n }\r\n }\r\n } as typeof URL;\r\n // 静的メソッドを引き継ぐ\r\n globalThis.URL.createObjectURL = OrigURL.createObjectURL;\r\n globalThis.URL.revokeObjectURL = OrigURL.revokeObjectURL;\r\n return () => { globalThis.URL = OrigURL; };\r\n}\r\n\r\n/** @deprecated Use Ssr.extractStateData() from @wcstack/state instead */\r\nexport function extractStateData(stateEl: any): Record<string, any> {\r\n const raw = (stateEl as any).__state;\r\n if (!raw || typeof raw !== 'object') return {};\r\n const data: Record<string, any> = {};\r\n for (const [key, value] of Object.entries(raw)) {\r\n if (!key.startsWith('$') && typeof value !== 'function') {\r\n data[key] = value;\r\n }\r\n }\r\n return data;\r\n}\r\n\r\nexport type BootstrapFunction = () => void;\r\nexport type ReadyFunction = (doc: Document) => Promise<void>;\r\n\r\nexport interface RenderOptions {\r\n /** 相対 URL を解決するベース URL (例: \"http://localhost:3001\") */\r\n baseUrl?: string;\r\n /** bootstrap 関数の配列。省略時は @wcstack/state を自動ロード */\r\n bootstraps?: BootstrapFunction[];\r\n /** バインディング等の非同期初期化完了を待機する関数の配列 */\r\n ready?: ReadyFunction[];\r\n}\r\n\r\nasync function loadDefaultBootstraps(): Promise<{\r\n bootstraps: BootstrapFunction[];\r\n ready: ReadyFunction[];\r\n}> {\r\n const { bootstrapState, getBindingsReady } = await import('@wcstack/state');\r\n return {\r\n bootstraps: [bootstrapState],\r\n ready: [(doc: Document) => getBindingsReady(doc as any)],\r\n };\r\n}\r\n\r\n/**\r\n * HTML 文字列を SSR レンダリングして返す。\r\n *\r\n * ## 入力 HTML のルール\r\n * - `<body>` の中身だけを渡す(`<html>`, `<head>`, `<body>` タグは含めない)\r\n * - `<script>` / `<link>` による外部リソース読み込みは実行されない\r\n * → 必要なパッケージは `options.bootstraps` で明示的に渡す\r\n *\r\n * ## SSR でできること\r\n *\r\n * ### 状態の初期化とデータ取得\r\n * - `<wcs-state>` の状態ロード(json 属性, src 属性, inline `<script type=\"module\">`)\r\n * - `$connectedCallback` でのサーバーサイド fetch(API 呼び出し、DB 問い合わせ等)\r\n *\r\n * ```html\r\n * <!-- JSON 直接指定 -->\r\n * <wcs-state enable-ssr json='{\"title\":\"Hello\"}'></wcs-state>\r\n *\r\n * <!-- $connectedCallback で API からデータ取得 -->\r\n * <!-- $connectedCallback は状態オブジェクトのメソッドとして定義し、this が state proxy -->\r\n * <wcs-state enable-ssr>\r\n * <script type=\"module\">\r\n * export default {\r\n * async $connectedCallback() {\r\n * const res = await fetch('/api/users');\r\n * this.users = await res.json();\r\n * }\r\n * };\r\n * </script>\r\n * </wcs-state>\r\n * ```\r\n *\r\n * ### wcs-fetch を使ったサーバー通信\r\n * - `<wcs-fetch>` の auto-fetch(`manual` なし)はサーバーでも実行される\r\n * - `manual` + `$connectedCallback` で明示的に制御する場合:\r\n *\r\n * ```html\r\n * <wcs-fetch id=\"api\" url=\"/api/users\" manual></wcs-fetch>\r\n * <wcs-state enable-ssr>\r\n * <script type=\"module\">\r\n * export default {\r\n * async $connectedCallback() {\r\n * const el = document.getElementById('api');\r\n * this.users = await el.fetch();\r\n * }\r\n * };\r\n * </script>\r\n * </wcs-state>\r\n * ```\r\n * ※ `bootstraps` に `bootstrapFetch` を含める必要あり\r\n *\r\n * ### バインディングと構造レンダリング\r\n * - `data-wcs` バインディングの適用(text, attribute, class, style, property)\r\n * - `<template data-wcs=\"for:\">` / `if:` / `elseif:` / `else:` の構造レンダリング\r\n *\r\n * ```html\r\n * <ul>\r\n * <template data-wcs=\"for: users\">\r\n * <li data-wcs=\"textContent: .name\"></li>\r\n * </template>\r\n * </ul>\r\n * <template data-wcs=\"if: isAdmin\">\r\n * <div class=\"admin-panel\">...</div>\r\n * </template>\r\n * ```\r\n *\r\n * ### ハイドレーション\r\n * - `enable-ssr` 付き `<wcs-state>` の `<wcs-ssr>` メタデータ自動生成\r\n * - クライアント側でのハイドレーション(再レンダリングなしでバインディング復元)\r\n * - `enable-ssr` を外した `<wcs-state>` はクライアントのみで動作(部分 CSR)\r\n *\r\n * ### カスタム要素の待機\r\n * - `static hasConnectedCallbackPromise = true` プロトコル準拠の全カスタム要素を自動待機\r\n *\r\n * ## SSR でできないこと\r\n * - `<head>` 内の `<script src=\"...\">` や `<link>` の自動実行\r\n * - ブラウザ固有 API(localStorage, sessionStorage, navigator 等)\r\n * - Shadow DOM のレンダリング(Declarative Shadow DOM 非対応)\r\n * - イベントハンドラの登録(クライアント側のハイドレーションで復元)\r\n * - `<wcs-autoloader>` による動的コンポーネント読み込み\r\n *\r\n * ## HTML の分割パターン\r\n * ```\r\n * // server.js\r\n * const ssrBody = await renderToString(template, { ... });\r\n * const page = `<!DOCTYPE html>\r\n * <html lang=\"ja\">\r\n * <head>\r\n * <script type=\"module\" src=\"/packages/state/dist/auto.js\"></script>\r\n * </head>\r\n * <body>${ssrBody}</body>\r\n * </html>`;\r\n * ```\r\n * `renderToString` には `<body>` の中身だけを渡し、\r\n * `<head>` や `<script>` タグは外側のテンプレートで囲む。\r\n */\r\nexport async function renderToString(html: string, options?: RenderOptions): Promise<string> {\r\n const window = new Window();\r\n const restoreGlobals = installGlobals(window);\r\n const document = window.document;\r\n\r\n // 相対 URL を baseUrl で解決する URL コンストラクタパッチをインストール\r\n const restoreBaseUrl = options?.baseUrl\r\n ? installBaseUrl(options.baseUrl)\r\n : null;\r\n\r\n // bootstrap / ready の解決\r\n const hasCustomBootstraps = options?.bootstraps !== undefined;\r\n const defaults = hasCustomBootstraps ? null : await loadDefaultBootstraps();\r\n const bootstraps = options?.bootstraps ?? defaults!.bootstraps;\r\n const readyFns = options?.ready ?? defaults?.ready ?? [];\r\n\r\n for (const bootstrap of bootstraps) {\r\n bootstrap();\r\n }\r\n\r\n try {\r\n\r\n // SSR モードを html 要素に設定\r\n document.documentElement.setAttribute('data-wcs-server', '');\r\n\r\n // HTML をパース\r\n // connectedCallback が自動発火 → state ロード → $connectedCallback 実行\r\n document.body.innerHTML = html;\r\n\r\n // connectedCallbackPromise プロトコル準拠の全カスタム要素の完了を待機\r\n const promises: Promise<void>[] = [];\r\n for (const el of document.querySelectorAll('*-*')) {\r\n const ctor = el.constructor as any;\r\n if (ctor.hasConnectedCallbackPromise) {\r\n promises.push((el as any).connectedCallbackPromise);\r\n }\r\n }\r\n await Promise.all(promises);\r\n\r\n // 非同期初期化の完了を待機\r\n await Promise.all(readyFns.map(fn => fn(document as any)));\r\n\r\n return document.body.innerHTML;\r\n } finally {\r\n restoreBaseUrl?.();\r\n restoreGlobals();\r\n await window.close();\r\n }\r\n}\r\n","import pkg from \"../package.json\" with { type: \"json\" };\r\nexport const VERSION = pkg.version;\r\n","import { renderToString } from \"./render.js\";\r\nimport { IWcBindable } from \"./types.js\";\r\n\r\nexport class RenderCore extends EventTarget {\r\n static wcBindable: IWcBindable = {\r\n protocol: \"wc-bindable\",\r\n version: 1,\r\n properties: [\r\n { name: \"html\", event: \"wcs-render:html-changed\" },\r\n { name: \"loading\", event: \"wcs-render:loading-changed\" },\r\n { name: \"error\", event: \"wcs-render:error\" },\r\n ],\r\n };\r\n\r\n private _html: string | null = null;\r\n private _loading: boolean = false;\r\n private _error: Error | null = null;\r\n\r\n get html(): string | null {\r\n return this._html;\r\n }\r\n\r\n get loading(): boolean {\r\n return this._loading;\r\n }\r\n\r\n get error(): Error | null {\r\n return this._error;\r\n }\r\n\r\n private _setLoading(loading: boolean): void {\r\n this._loading = loading;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:loading-changed\", {\r\n detail: loading,\r\n }));\r\n }\r\n\r\n private _setHtml(html: string): void {\r\n this._html = html;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:html-changed\", {\r\n detail: html,\r\n }));\r\n }\r\n\r\n private _setError(error: Error): void {\r\n this._error = error;\r\n this.dispatchEvent(new CustomEvent(\"wcs-render:error\", {\r\n detail: error,\r\n }));\r\n }\r\n\r\n async render(html: string): Promise<string | null> {\r\n this._setLoading(true);\r\n this._error = null;\r\n\r\n try {\r\n const result = await renderToString(html);\r\n this._setHtml(result);\r\n this._setLoading(false);\r\n return this._html;\r\n } catch (e: any) {\r\n this._setError(e instanceof Error ? e : new Error(String(e)));\r\n this._setLoading(false);\r\n return null;\r\n }\r\n }\r\n}\r\n"],"names":["GLOBALS_KEYS","installGlobals","window","saved","key","globalThis","origCreateObjectURL","URL","createObjectURL","undefined","installBaseUrl","baseUrl","OrigURL","base","constructor","input","inputBase","startsWith","super","revokeObjectURL","extractStateData","stateEl","raw","__state","data","value","Object","entries","async","renderToString","html","options","Window","restoreGlobals","document","restoreBaseUrl","defaults","bootstraps","bootstrapState","getBindingsReady","import","ready","doc","loadDefaultBootstraps","readyFns","bootstrap","documentElement","setAttribute","body","innerHTML","promises","el","querySelectorAll","hasConnectedCallbackPromise","push","connectedCallbackPromise","Promise","all","map","fn","close","VERSION","RenderCore","EventTarget","static","protocol","version","properties","name","event","_html","_loading","_error","this","loading","error","_setLoading","dispatchEvent","CustomEvent","detail","_setHtml","_setError","render","result","e","Error","String"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wcstack/server",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Server-side rendering for wcstack Web Components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.esm.js",
|
|
7
|
+
"module": "./dist/index.esm.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.esm.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rimraf dist .tsc-out",
|
|
20
|
+
"build": "rimraf dist .tsc-out && tsc && rollup -c",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
25
|
+
"lint": "eslint src"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"web-components",
|
|
29
|
+
"ssr",
|
|
30
|
+
"server-side-rendering",
|
|
31
|
+
"wcstack"
|
|
32
|
+
],
|
|
33
|
+
"author": "mogera551",
|
|
34
|
+
"homepage": "https://wcstack.github.io",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/wcstack/wcstack.git",
|
|
38
|
+
"directory": "packages/server"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/wcstack/wcstack/issues"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@wcstack/state": "file:../state",
|
|
46
|
+
"happy-dom": "^20.0.11"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
50
|
+
"rimraf": "^6.0.1",
|
|
51
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
52
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
53
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
54
|
+
"rollup": "^4.22.4",
|
|
55
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
56
|
+
"tslib": "^2.8.1",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"vitest": "^4.0.15"
|
|
59
|
+
}
|
|
60
|
+
}
|