@wcstack/state 2.1.0 → 2.1.1
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 +85 -17
- package/README.md +88 -19
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.esm.js +31 -13
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
- 変数を取り出す API
|
|
16
16
|
- 要素ごとに状態を束縛するオブジェクト
|
|
17
|
-
- hook
|
|
17
|
+
- hook(`useState` / `useStore` 系。`$connectedCallback` などのライフサイクルコールバックはこれに当たりません)
|
|
18
18
|
- selector
|
|
19
19
|
- reactive primitive をコンポーネントへ引き込むための glue code
|
|
20
20
|
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## 位置づけ — 選ぶとき・選ばないとき
|
|
26
26
|
|
|
27
|
-
これは React / Vue / Solid
|
|
27
|
+
これは React / Vue / Solid の別構文ではありません。あちらは UI と状態の結合点をコンポーネントの中に置き、こちらはパス文字列に置きます。**前提自体が違う**ので、比較は正しい軸で行ったときにだけ意味を持ちます。
|
|
28
28
|
|
|
29
|
-
|
|
|
29
|
+
| コンポーネント型 FW が前提にするもの | `@wcstack/state` が前提にするもの |
|
|
30
30
|
|---|---|
|
|
31
31
|
| コンポーネントが UI と状態の結合点 | パス文字列が UI と状態の結合点 |
|
|
32
32
|
| JavaScript が描画の中心 | HTML と DOM が中心 |
|
|
@@ -34,7 +34,16 @@
|
|
|
34
34
|
| hook / selector / signal で購読する | 属性とパスで束縛する |
|
|
35
35
|
| フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
より近い親戚は **属性ディレクティブ型・ビルド不要のライブラリ** — Alpine.js や petite-vue の系統です。前提(素の HTML への属性・コンパイラ不要)は共有しつつ、選択を分ける違いが 2 つあります。
|
|
38
|
+
|
|
39
|
+
- **式言語を持たない。** それらは属性に JavaScript 式を書き、実行時に評価します。`data-wcs` に載るのはパスとフィルタチェーンだけで、計算は state 側のパス getter に置きます。バインディングを静的に検査できる(`@wcstack/lint`・VS Code 拡張・`@wcstack/typescript`)のも、`unsafe-eval` なしの厳格な CSP で動く([docs/csp.ja.md](../../docs/csp.ja.md))のも、この選択の帰結です。
|
|
40
|
+
- **Web Components 同士を配線する。** wc-bindable・command-token・event-token の各プロトコルと `bind-component` マウントが、互いを import しない要素同士を接続します。[I/O ノード群](../../README.ja.md#追加パッケージ)はその上に成り立っています。
|
|
41
|
+
|
|
42
|
+
**選ぶとき**: HTML が主役のページ — サーバー描画や静的なマークアップにリアクティブな部分を足す、カスタム要素を組み合わせてページを作る、「HTML を読めばデータ依存が全部わかる」ことに価値があり、ビルド工程が前提ではなくコストであるとき。
|
|
43
|
+
|
|
44
|
+
**選ばないとき**: チームが既にコンポーネント型 FW の中で暮らしている — I/O ノードは[フレームワークアダプタ](../../docs/framework-adapter-integration.ja.md)経由で使ってください。ホットパスが巨大な keyed リスト — [パフォーマンス](#パフォーマンス)節の計測では生成・追加が [`@wcstack/signals`](../signals/) の 2.5〜3.5 倍で、相互運用できる signals のほうが適任です。テンプレートに式を書きたい — 意図的に存在しません。テンプレートの型検査をツールではなくコンパイラに求める — パスは文字列で、`@wcstack/typescript` は差を縮めますが埋めはしません。
|
|
45
|
+
|
|
46
|
+
この軸に乗せれば比較は具体的になります。下の[パフォーマンス](#パフォーマンス)節がその一例で、`e2e/bench/` のドライバで手元のハードウェアでも再現できます。
|
|
38
47
|
|
|
39
48
|
## 第一原理: パスが唯一の契約
|
|
40
49
|
|
|
@@ -95,6 +104,9 @@
|
|
|
95
104
|
- **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
|
|
96
105
|
- **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
|
|
97
106
|
- **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
|
|
107
|
+
- **ボリューム** — `<wcs-state mount="cart">` がモジュールを 1 本の state ツリーに接ぎ木し、バインディングは `cart.…` で読む
|
|
108
|
+
- **行の同一性** — `$listKeys` が再取得した配列でも行の DOM と行オブジェクトを保つ
|
|
109
|
+
- **ワイルドカード集計** — `$getAll` / `$setAll` が配列を作り直さずに `items.*.price` を横断して読み書きする
|
|
98
110
|
- **組み込みフィルタ** — フォーマット、比較、算術、日付など 46 種類
|
|
99
111
|
- **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
|
|
100
112
|
- **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
|
|
@@ -106,7 +118,9 @@
|
|
|
106
118
|
- **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
|
|
107
119
|
- **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
|
|
108
120
|
- **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
|
|
109
|
-
- **
|
|
121
|
+
- **headless な watch** — `$watch` はパスが描画されていてもいなくても state の変化で発火する
|
|
122
|
+
- **診断** — 解決しないパス・添字の本数・getter の循環を `@wcstack/lint`・VS Code 拡張と同じ診断 code で報告する
|
|
123
|
+
- **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))。`@wcstack/typescript` は同じ型を HTML の検証器へ運び(`wcs-schema`)、インライン state スクリプトを型検査する(`wcs-tsc`)— [docs/typescript.ja.md](../../docs/typescript.ja.md)
|
|
110
124
|
- **サーバーサイドレンダリング** — `enable-ssr` 属性 + `@wcstack/server` でフル SSR と自動ハイドレーション
|
|
111
125
|
- **依存ゼロ** — ランタイム依存なし
|
|
112
126
|
|
|
@@ -233,7 +247,7 @@
|
|
|
233
247
|
<div data-wcs="textContent: cart.total"></div>
|
|
234
248
|
```
|
|
235
249
|
|
|
236
|
-
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@`
|
|
250
|
+
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
|
|
237
251
|
|
|
238
252
|
> **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
|
|
239
253
|
|
|
@@ -359,6 +373,34 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
359
373
|
|
|
360
374
|
> **作法:** settable なメンバは **`properties` と `inputs` の両方**に宣言してください。`properties` にしか宣言されていないメンバは output-only 扱いになり、state→element 書き込みがバインディングの生存期間ずっと抑止され、要素側の初期値が state 側のシード値を上書きします(`@wcstack` の I/O ノード Shell と DCC の `$bindables` はこの作法に従っています)。
|
|
361
375
|
|
|
376
|
+
#### 要素から state に何が書かれるか(`properties[].getter`)
|
|
377
|
+
|
|
378
|
+
要素が `properties[].event` を dispatch したとき、state に書かれる値は **`getter(event)`** です。`getter` を省略するとプロトコル既定の [`(e) => e.detail`](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/SPEC.md#default-getter) が適用され、**`detail` 全体がそのまま**書かれます。このとき宣言したプロパティは要素から読まれ*ません*。イベントのペイロードが正です。`wcBindable` を持たない素の HTML 要素は逆で、`input`/`change` 時に `element[propName]` を読みます。
|
|
379
|
+
|
|
380
|
+
したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state に書きます。しかもこの失敗は無言です。書き戻し(`Number({ value: … })` → `NaN`)は警告を出さず、`@wcstack/lint` にも見えません(ペイロードの形は静的に分かりません)。次の 2 形のどちらかに揃えてください:
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
class YenInput extends HTMLElement {
|
|
384
|
+
static wcBindable = {
|
|
385
|
+
protocol: "wc-bindable", version: 1,
|
|
386
|
+
properties: [
|
|
387
|
+
// (a) 値そのものを detail にする — プロトコルの推奨形。getter 不要
|
|
388
|
+
{ name: "value", event: "yen-input:value-changed" },
|
|
389
|
+
// (b) detail がオブジェクト、または CustomEvent でないイベントを使う — 読み方を宣言する
|
|
390
|
+
// { name: "value", event: "yen-input:value-changed", getter: (e) => e.detail.value },
|
|
391
|
+
// { name: "value", event: "input", getter: (e) => e.target.value },
|
|
392
|
+
],
|
|
393
|
+
inputs: [{ name: "value" }],
|
|
394
|
+
};
|
|
395
|
+
#onInput() {
|
|
396
|
+
// (a): ラッパーオブジェクトではなく値を dispatch する
|
|
397
|
+
this.dispatchEvent(new CustomEvent("yen-input:value-changed", { detail: this.value, bubbles: true }));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
どちらを選んでも、`element.value` とイベントから取り出す値は同じ論理状態を表していなければなりません(プロトコルの *Producer State Consistency Invariant*)。初期同期はプロパティを読み、以後の更新はイベントを読むからです。wcstack 内部でも両形が使われています —— `<wcs-fetch>` の `loading` は `getter` なしで真偽値を `detail` に載せ、`value` は `getter` で `detail.value` を読みます。DCC の `$bindables` が `getter: (e) => e.target[name]` を宣言するのは、サブパス書き込みには `detail` に載せる単一の値が無いからです。既定そのものを変える予定はありません。`e.detail` はすべての wc-bindable アダプタに対する規範(`@wc-bindable/core` の `bind()` とフレームワークアダプタも同じ既定)で、プロトコル上、既定の変更は新しいプロトコル識別子を要する破壊的変更に分類されています。
|
|
403
|
+
|
|
362
404
|
authority はバインディング単位で `#init=` により上書きできます:
|
|
363
405
|
|
|
364
406
|
| 値 | 初期同期 | 使える宣言 |
|
|
@@ -2114,7 +2156,7 @@ export default {
|
|
|
2114
2156
|
- フック内の `this` は読み書き可能な状態プロキシです。
|
|
2115
2157
|
- `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
|
|
2116
2158
|
- `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
|
|
2117
|
-
- `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath`
|
|
2159
|
+
- `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
|
|
2118
2160
|
- Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
|
|
2119
2161
|
|
|
2120
2162
|
## 遷移アニメーション
|
|
@@ -2149,7 +2191,7 @@ li {
|
|
|
2149
2191
|
配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
|
|
2150
2192
|
|
|
2151
2193
|
```
|
|
2152
|
-
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on state
|
|
2194
|
+
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
|
|
2153
2195
|
"nmae" is not declared. Did you mean "name"? Updates to this path will be silently
|
|
2154
2196
|
dropped. Validate statically: npx @wcstack/lint <file>.
|
|
2155
2197
|
```
|
|
@@ -2233,12 +2275,14 @@ bootstrapState({
|
|
|
2233
2275
|
|---|---|---|
|
|
2234
2276
|
| `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
|
|
2235
2277
|
| `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
|
|
2278
|
+
| `tagNames.ssr` | `'wcs-ssr'` | SSR ハイドレーションデータ要素のタグ名 |
|
|
2236
2279
|
| `locale` | `<html lang>`、無ければ `'en'` | ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)のロケール — [ロケール](#ロケール)を参照 |
|
|
2237
2280
|
| `debug` | `false` | デバッグモード |
|
|
2238
2281
|
| `enableMustache` | `true` | `{{ }}` 構文の有効化 |
|
|
2239
2282
|
| `enableDirectionalInitialSync` | `true` | 方向認識のバインディング authority(`#init=` / `#sync=` バインド modifier)— [バインディング authority](#バインディング-authority-init--sync) 参照。既定 on。`false` で opt-out |
|
|
2240
2283
|
| `enablePropagationContext` | `true` | バインド間の因果伝播トラッキング(echo/diamond のループ防止)。既定 on。`false` で opt-out |
|
|
2241
2284
|
| `enableContractAnalyzer` | `false` | opt-in の開発時 contract analyzer(`analyzeContract` を公開) |
|
|
2285
|
+
| `sameValueGuard` | `true` | 現在値と `Object.is` で同値なプリミティブ書き込みを enqueue 前に落とす — バインディングと `$watch` は実質「変化時のみ」発火する(参照型は常に通す)。`false` で同値書き込みを通し、`$watch` の `prev` は `undefined` になる |
|
|
2242
2286
|
|
|
2243
2287
|
### ロケール
|
|
2244
2288
|
|
|
@@ -2421,6 +2465,8 @@ export default defineState({
|
|
|
2421
2465
|
|
|
2422
2466
|
ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
|
|
2423
2467
|
|
|
2468
|
+
`defineState()` が型を付けるのは state ファイルです。その型を HTML まで運ぶのが [`@wcstack/typescript`](../typescript/README.ja.md) の 2 つの CLI で、`wcs-schema` は `@wcstack/lint` と VS Code 拡張が `data-wcs` パスの検証に使う `stateSchema` sidecar を書き出し、`wcs-tsc` はインラインの `<script type="module">` state に TypeScript コンパイラを掛けます。全体像は [docs/typescript.ja.md](../../docs/typescript.ja.md)。
|
|
2469
|
+
|
|
2424
2470
|
## API リファレンス
|
|
2425
2471
|
|
|
2426
2472
|
### `bootstrapState()`
|
|
@@ -2432,6 +2478,21 @@ import { bootstrapState } from '@wcstack/state';
|
|
|
2432
2478
|
bootstrapState();
|
|
2433
2479
|
```
|
|
2434
2480
|
|
|
2481
|
+
### その他のエクスポート
|
|
2482
|
+
|
|
2483
|
+
| エクスポート | 説明 |
|
|
2484
|
+
|---|---|
|
|
2485
|
+
| `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗すれば reject |
|
|
2486
|
+
| `buildBindings(root)` | `document` / `ShadowRoot` 配下のバインディングを明示的に構築する — その root に最初に登録された `<wcs-state>` がスケジュールするもの |
|
|
2487
|
+
| `getConfig()` | 現在の設定(読み取り専用ビュー) |
|
|
2488
|
+
| `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照 |
|
|
2489
|
+
| `VERSION` | パッケージのバージョン。`<wcs-ssr>` に刻印され、ハイドレーション時に照合される |
|
|
2490
|
+
| `getWcsManifest()` / `WCS_MANIFEST_VERSION` | バインディング構文・組み込みフィルタ・予約名の機械可読 manifest — 実装から導出され、`@wcstack/lint` と VS Code 拡張が消費する |
|
|
2491
|
+
| `builtinFilterMeta` | 全組み込みフィルタの引数・戻り値メタデータ |
|
|
2492
|
+
| `analyzeContract()` | 開発時の contract analyzer。`enableContractAnalyzer` が off なら no-op |
|
|
2493
|
+
|
|
2494
|
+
ツール向けのサブパスエントリ: `@wcstack/state/parser`(`data-wcs` パーサを DOM 非依存の純関数として公開)、`@wcstack/state/manifest`、`@wcstack/state/wcs-manifest.json`(manifest をビルド済み JSON として公開)。
|
|
2495
|
+
|
|
2435
2496
|
### `<wcs-state>` 要素
|
|
2436
2497
|
|
|
2437
2498
|
| 属性 | 説明 |
|
|
@@ -2441,19 +2502,20 @@ bootstrapState();
|
|
|
2441
2502
|
| `src` | `.json` または `.js` ファイルの URL |
|
|
2442
2503
|
| `json` | インライン JSON 文字列 |
|
|
2443
2504
|
| `bind-component` | Web Component バインディングのプロパティ名 |
|
|
2505
|
+
| `enable-ssr` | SSR を有効化: サーバーはこの state の `<wcs-ssr>` ハイドレーションデータを出力し、クライアントは再描画せずそこから復元する — [サーバーサイドレンダリング](#サーバーサイドレンダリング) 参照 |
|
|
2444
2506
|
|
|
2445
2507
|
### IStateElement
|
|
2446
2508
|
|
|
2447
2509
|
| プロパティ / メソッド | 説明 |
|
|
2448
2510
|
|---|---|
|
|
2449
2511
|
| `initializePromise` | 状態の完全な初期化時に解決される Promise |
|
|
2512
|
+
| `connectedCallbackPromise` | `connectedCallback` の完了(state のロードと `$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの |
|
|
2450
2513
|
| `listPaths` | `for` ループで使用されるパスの Set |
|
|
2451
2514
|
| `getterPaths` | getter として定義されたパスの Set |
|
|
2452
2515
|
| `setterPaths` | setter として定義されたパスの Set |
|
|
2453
2516
|
| `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
|
|
2454
2517
|
| `createStateAsync(mutability, callback)` | `createState` の非同期版 |
|
|
2455
2518
|
| `setInitialState(state)` | プログラムから状態を設定(初期化前) |
|
|
2456
|
-
| `bindProperty(prop, descriptor)` | 生の状態オブジェクトにプロパティを定義 |
|
|
2457
2519
|
| `nextVersion()` | バージョン番号をインクリメントして返す |
|
|
2458
2520
|
|
|
2459
2521
|
## アーキテクチャ
|
|
@@ -2463,11 +2525,14 @@ bootstrapState()
|
|
|
2463
2525
|
└── registerComponents() // <wcs-state> カスタム要素を登録
|
|
2464
2526
|
|
|
2465
2527
|
<wcs-state> connectedCallback
|
|
2466
|
-
├──
|
|
2467
|
-
├──
|
|
2468
|
-
│
|
|
2469
|
-
│
|
|
2470
|
-
│
|
|
2528
|
+
├── 置かれ方によりいずれか 1 つ:
|
|
2529
|
+
│ ├── _initializeDCC() // data-wc-definition ホスト配下: DCC クラスを定義
|
|
2530
|
+
│ ├── _initializeVolume() // mount=: このボリュームをルートツリーへ接ぎ木
|
|
2531
|
+
│ ├── _initializeBindWebComponent() // bind-component: ホストのツリーをマウント点でエイリアス
|
|
2532
|
+
│ └── _initialize() // ルート: 状態をロード (state属性 / src / json / script / API)
|
|
2533
|
+
│ └── setStateElement() // WeakMap<Node, IStateElement> に登録 — 1 root 1 ツリー
|
|
2534
|
+
│ └── (rootNode への初回登録時)
|
|
2535
|
+
│ └── queueMicrotask → buildBindings()
|
|
2471
2536
|
├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
|
|
2472
2537
|
|
|
2473
2538
|
buildBindings(root)
|
|
@@ -2491,7 +2556,7 @@ buildBindings(root)
|
|
|
2491
2556
|
- **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
|
|
2492
2557
|
- **ListIndex** — ランタイムループインデックスチェーン
|
|
2493
2558
|
- **StateAddress** — PathInfo + ListIndex の組み合わせ
|
|
2494
|
-
- **AbsoluteStateAddress** —
|
|
2559
|
+
- **AbsolutePathInfo / AbsoluteStateAddress** — ツリーを持つ state 要素に固定した PathInfo と、その ListIndex の組。マウントされたコンポーネントとボリュームは相対パスをこの層でホストのツリーへ翻訳する。v2 は 1 root 1 ツリーなので、アドレスに状態名はない
|
|
2495
2560
|
|
|
2496
2561
|
## パフォーマンス
|
|
2497
2562
|
|
|
@@ -2531,7 +2596,10 @@ buildBindings(root)
|
|
|
2531
2596
|
し、残りのページは宣言的なまま保てます。
|
|
2532
2597
|
- クリア後に残るヒープは、次のリスト生成を安くする有界の行プールです。
|
|
2533
2598
|
|
|
2534
|
-
絶対値は 1
|
|
2599
|
+
絶対値は 1 台の開発機で v1.21.6 + PR#87(clear リーク修正)時点に取ったものです。
|
|
2600
|
+
v2.0 のマウント作業は同じドライバの同一セッション A/B でゲートし、実行ごとの
|
|
2601
|
+
ノイズ内に収まった([docs/state-mount-impl-plan.md](../../docs/state-mount-impl-plan.md)
|
|
2602
|
+
§2-2 と slice 27)ため、表は取り直していません。絶対値はマシン状態で ±20% 揺れます。
|
|
2535
2603
|
`e2e/bench/` のドライバで手元のハードウェアでも再現できます。
|
|
2536
2604
|
|
|
2537
2605
|
## サーバーサイドレンダリング
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The following are not missing features. **They do not exist by design.**
|
|
|
14
14
|
|
|
15
15
|
- APIs for pulling variables out of state into components
|
|
16
16
|
- Per-element binding objects that mediate state access
|
|
17
|
-
- hooks
|
|
17
|
+
- hooks (`useState` / `useStore`-style — the `$connectedCallback` lifecycle callbacks are not that)
|
|
18
18
|
- selectors
|
|
19
19
|
- glue code that imports reactive primitives into component code
|
|
20
20
|
|
|
@@ -22,11 +22,11 @@ None of these exist by design.
|
|
|
22
22
|
|
|
23
23
|
Why: this library does not put the UI-state coupling point inside JavaScript. State is not pulled into components. HTML refers to state through path strings. Elements do not own state, and state does not know elements. The only shared contract is the path.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Where It Sits — and When Not to Choose It
|
|
26
26
|
|
|
27
|
-
This is not
|
|
27
|
+
This is not React / Vue / Solid with a different syntax. Those put the coupling point between UI and state inside a component; this puts it in a path string. **The premises are different**, and a comparison only says something when it is made along the right axis.
|
|
28
28
|
|
|
29
|
-
| What
|
|
29
|
+
| What component frameworks assume | What `@wcstack/state` assumes |
|
|
30
30
|
|---|---|
|
|
31
31
|
| Components are the coupling point between UI and state | Path strings are the coupling point between UI and state |
|
|
32
32
|
| JavaScript is the center of rendering | HTML and the DOM are the center |
|
|
@@ -34,7 +34,16 @@ This is not solving the same problem as React / Vue / Solid with a different syn
|
|
|
34
34
|
| hooks / selectors / signals express subscriptions | Attributes and paths express bindings |
|
|
35
35
|
| The whole app runs inside a framework execution model | A thin reactive layer is added on top of web standards |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
The nearer relatives are the **attribute-directive, no-build libraries** — Alpine.js, petite-vue and their kind. They share the premise (attributes on plain HTML, no compiler) and differ on two points that decide the choice:
|
|
38
|
+
|
|
39
|
+
- **No expression language.** Those libraries put JavaScript expressions in attributes and evaluate them at runtime. `data-wcs` carries a path and a filter chain, nothing else; computation lives in path getters on the state. That is what lets a binding be checked statically (`@wcstack/lint`, the VS Code extension, `@wcstack/typescript`) and lets a page run under a strict CSP with no `unsafe-eval` ([docs/csp.md](../../docs/csp.md)).
|
|
40
|
+
- **It wires Web Components to each other.** The wc-bindable, command-token and event-token protocols and `bind-component` mounts connect elements that never import one another. The [I/O node packages](../../README.md#additional-packages) are what that buys.
|
|
41
|
+
|
|
42
|
+
**Choose it** for HTML-first pages: server-rendered or static markup with reactive parts, a page composed from custom elements, anywhere "read the HTML and know every data dependency" matters and a build step is a cost rather than a given.
|
|
43
|
+
|
|
44
|
+
**Do not choose it** when the team already lives inside a component framework — use the I/O nodes through the [framework adapters](../../docs/framework-adapter-integration.md) instead; when the hot path is a very large keyed list — [Performance](#performance) measures create / append at 2.5–3.5× [`@wcstack/signals`](../signals/), which interoperates with this package and is the better fit there; when templates need inline expressions — deliberately absent; or when the template must be type-checked by the compiler rather than by tooling — paths are strings, and `@wcstack/typescript` narrows that gap without closing it.
|
|
45
|
+
|
|
46
|
+
On those axes the comparison is concrete: the [Performance](#performance) section below is one, and the drivers under `e2e/bench/` regenerate it on your own hardware.
|
|
38
47
|
|
|
39
48
|
## First Principle: Path as the Universal Contract
|
|
40
49
|
|
|
@@ -95,6 +104,9 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
95
104
|
- **Declarative data binding** — `data-wcs` attribute for property / text / event / structural binding
|
|
96
105
|
- **Reactive Proxy** — ES Proxy-based automatic DOM updates with dependency tracking
|
|
97
106
|
- **Structural directives** — `for`, `if` / `elseif` / `else` via `<template>` elements
|
|
107
|
+
- **Volumes** — `<wcs-state mount="cart">` grafts a module onto the single state tree; bindings read it as `cart.…`
|
|
108
|
+
- **Row identity** — `$listKeys` keeps row DOM and row objects across refetched arrays
|
|
109
|
+
- **Wildcard aggregation** — `$getAll` / `$setAll` read and write across `items.*.price` without rebuilding the array
|
|
98
110
|
- **Built-in filters** — 46 filters for formatting, comparison, arithmetic, date, and more
|
|
99
111
|
- **Two-way binding** — automatic for `<input>`, `<select>`, `<textarea>`
|
|
100
112
|
- **Web Component binding** — bidirectional state binding with Shadow DOM components
|
|
@@ -106,7 +118,9 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
106
118
|
- **Multiple state sources** — JSON, JS module, inline script, API, attribute
|
|
107
119
|
- **SVG support** — full binding support inside `<svg>` elements
|
|
108
120
|
- **Lifecycle hooks** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`, plus `$stateReadyCallback` for Web Components
|
|
109
|
-
- **
|
|
121
|
+
- **Headless watch** — `$watch` fires on state changes whether or not the path is rendered
|
|
122
|
+
- **Diagnostics** — unresolved paths, index arity and getter cycles are reported with the same codes as `@wcstack/lint` and the VS Code extension
|
|
123
|
+
- **TypeScript support** — `defineState()` for typed state definitions with dot-path autocompletion ([details](docs/define-state.md)); `@wcstack/typescript` carries the same types into the HTML validator (`wcs-schema`) and type-checks inline state scripts (`wcs-tsc`) — see [docs/typescript.md](../../docs/typescript.md)
|
|
110
124
|
- **Server-Side Rendering** — `enable-ssr` attribute + `@wcstack/server` for full SSR with automatic hydration
|
|
111
125
|
- **Zero dependencies** — no runtime dependencies
|
|
112
126
|
|
|
@@ -233,7 +247,7 @@ There is **one state tree per root**. To split state across modules, mount a vol
|
|
|
233
247
|
<div data-wcs="textContent: cart.total"></div>
|
|
234
248
|
```
|
|
235
249
|
|
|
236
|
-
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. Load order does not matter (a volume connected before the root is grafted when the root registers). Mount paths must be static (`*`, `$`, `#`, `@` are rejected).
|
|
250
|
+
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. Load order does not matter (a volume connected before the root is grafted when the root registers). Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
|
|
237
251
|
|
|
238
252
|
> **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
|
|
239
253
|
|
|
@@ -359,6 +373,34 @@ For custom elements that declare `static wcBindable`, every prop binding resolve
|
|
|
359
373
|
|
|
360
374
|
> **Authoring rule:** declare every settable member in **both** `properties` and `inputs`. A member declared only in `properties` is output-only — state→element writes are suppressed for the life of the binding, and the element's own initial value overwrites whatever the state seeded. (`@wcstack` I/O node Shells and DCC `$bindables` follow this rule.)
|
|
361
375
|
|
|
376
|
+
#### What the element writes back (`properties[].getter`)
|
|
377
|
+
|
|
378
|
+
When the element dispatches `properties[].event`, the value written to state is **`getter(event)`**. With no `getter`, the protocol default applies — [`(e) => e.detail`](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/SPEC.md#default-getter): the **whole `detail`, as-is**. The declared property is *not* read off the element at that point; the event payload is authoritative. A plain HTML element (no `wcBindable`) is the other way round: `element[propName]` is read on `input`/`change`.
|
|
379
|
+
|
|
380
|
+
So an element that dispatches `detail: { value: 7654321 }` without a `getter` writes the **object** `{ value: 7654321 }` to state, not the number — and the failure is silent: the write-back (`Number({ value: … })` → `NaN`) produces no warning, and `@wcstack/lint` cannot see it (the payload shape is not static). Use one of the two conforming shapes:
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
class YenInput extends HTMLElement {
|
|
384
|
+
static wcBindable = {
|
|
385
|
+
protocol: "wc-bindable", version: 1,
|
|
386
|
+
properties: [
|
|
387
|
+
// (a) the value itself is the detail — the protocol's recommendation; no getter needed
|
|
388
|
+
{ name: "value", event: "yen-input:value-changed" },
|
|
389
|
+
// (b) the detail is an object, or the event is not a CustomEvent — say how to read it
|
|
390
|
+
// { name: "value", event: "yen-input:value-changed", getter: (e) => e.detail.value },
|
|
391
|
+
// { name: "value", event: "input", getter: (e) => e.target.value },
|
|
392
|
+
],
|
|
393
|
+
inputs: [{ name: "value" }],
|
|
394
|
+
};
|
|
395
|
+
#onInput() {
|
|
396
|
+
// (a): dispatch the value, not a wrapper object
|
|
397
|
+
this.dispatchEvent(new CustomEvent("yen-input:value-changed", { detail: this.value, bubbles: true }));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Whichever you pick, `element.value` and the value extracted from the event must be the same logical state (the protocol's *Producer State Consistency Invariant*): the initial sync reads the property, every later update reads the event. Both shapes are in use inside wcstack — `<wcs-fetch>`'s `loading` dispatches the boolean as `detail` with no `getter`, its `value` reads `detail.value` through one — and DCC `$bindables` declare `getter: (e) => e.target[name]` because a sub-path write has no single value to put in `detail`. The default itself is not going to change: it is normative for every wc-bindable adapter (`@wc-bindable/core`'s `bind()` and the framework adapters implement the same `e.detail`), and the protocol classes a different default as a breaking change requiring a new protocol identifier.
|
|
403
|
+
|
|
362
404
|
Override the authority per binding with `#init=`:
|
|
363
405
|
|
|
364
406
|
| Value | Initial sync | Allowed on |
|
|
@@ -2118,7 +2160,7 @@ All hooks except `$disconnectedCallback` support `async` — you can use `async/
|
|
|
2118
2160
|
- `this` inside hooks is the state proxy with full read/write access
|
|
2119
2161
|
- `$connectedCallback` is called **every time** the element is connected (including re-insertion after removal), making it suitable for setup that should be re-established
|
|
2120
2162
|
- `$disconnectedCallback` is called synchronously — use it for cleanup such as clearing timers, removing event listeners, or releasing resources
|
|
2121
|
-
- `$updatedCallback(paths, indexesListByPath)` receives the paths whose live bindings were applied in that drain. Unbound state writes do not invoke it or appear in `paths`. For wildcard updates, `indexesListByPath` contains the updated index sets. Can be `async`, but the return value is not awaited
|
|
2163
|
+
- `$updatedCallback(paths, indexesListByPath)` receives the paths whose live bindings were applied in that drain. Unbound state writes do not invoke it or appear in `paths`. For wildcard updates, `indexesListByPath` contains the updated index sets. Marker paths of mounted components (`#m…`) never appear in `paths` — a component's private keys stay private (DevTools shows them in its overlays view). Can be `async`, but the return value is not awaited
|
|
2122
2164
|
- In Web Components, define `async $stateReadyCallback(stateProp)` to receive a hook when the bound state becomes available via `bind-component`
|
|
2123
2165
|
|
|
2124
2166
|
## Transition animations
|
|
@@ -2153,7 +2195,7 @@ Only a batch that actually has bindings to apply is handed to the tag, so a writ
|
|
|
2153
2195
|
When a wired path provably does not resolve against the state, you get one warning at binding time (at declaration time for `$watch`). The diagnostic codes are shared by the console, `@wcstack/lint`, and the VS Code extension:
|
|
2154
2196
|
|
|
2155
2197
|
```
|
|
2156
|
-
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on state
|
|
2198
|
+
[@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
|
|
2157
2199
|
"nmae" is not declared. Did you mean "name"? Updates to this path will be silently
|
|
2158
2200
|
dropped. Validate statically: npx @wcstack/lint <file>.
|
|
2159
2201
|
```
|
|
@@ -2237,12 +2279,14 @@ All options with defaults:
|
|
|
2237
2279
|
|---|---|---|
|
|
2238
2280
|
| `bindAttributeName` | `'data-wcs'` | Binding attribute name |
|
|
2239
2281
|
| `tagNames.state` | `'wcs-state'` | State element tag name |
|
|
2282
|
+
| `tagNames.ssr` | `'wcs-ssr'` | Tag name of the SSR hydration-data element |
|
|
2240
2283
|
| `locale` | `<html lang>`, else `'en'` | Locale for the locale-dependent filters (`locale` / `date` / `time` / `datetime`) — see [Locale](#locale) |
|
|
2241
2284
|
| `debug` | `false` | Debug mode |
|
|
2242
2285
|
| `enableMustache` | `true` | Enable `{{ }}` syntax |
|
|
2243
2286
|
| `enableDirectionalInitialSync` | `true` | Direction-aware binding authority (`#init=` / `#sync=` binding modifiers) — see [Binding Authority](#binding-authority-init--sync). Default on; set `false` to opt out |
|
|
2244
2287
|
| `enablePropagationContext` | `true` | Causal propagation tracking across bindings (echo/diamond loop prevention). Default on; set `false` to opt out |
|
|
2245
2288
|
| `enableContractAnalyzer` | `false` | Opt-in dev-time contract analyzer (exposes `analyzeContract`) |
|
|
2289
|
+
| `sameValueGuard` | `true` | Drop a primitive write whose value is `Object.is`-equal to the current one before anything is enqueued — bindings and `$watch` effectively fire on change only; reference types always pass. `false` lets equal writes through and makes `$watch`'s `prev` `undefined` |
|
|
2246
2290
|
|
|
2247
2291
|
### Locale
|
|
2248
2292
|
|
|
@@ -2428,6 +2472,8 @@ export default defineState({
|
|
|
2428
2472
|
|
|
2429
2473
|
Utility types `WcsPaths<T>` and `WcsPathValue<T, P>` are also exported for advanced use cases. See [docs/define-state.md](docs/define-state.md) for full documentation.
|
|
2430
2474
|
|
|
2475
|
+
`defineState()` types the state file. To carry those types into the HTML, [`@wcstack/typescript`](../typescript/README.md) adds two CLIs: `wcs-schema` writes the `stateSchema` sidecar that `@wcstack/lint` and the VS Code extension validate `data-wcs` paths against, and `wcs-tsc` runs the TypeScript compiler over inline `<script type="module">` state. The whole story is in [docs/typescript.md](../../docs/typescript.md).
|
|
2476
|
+
|
|
2431
2477
|
## API Reference
|
|
2432
2478
|
|
|
2433
2479
|
### `bootstrapState()`
|
|
@@ -2439,6 +2485,21 @@ import { bootstrapState } from '@wcstack/state';
|
|
|
2439
2485
|
bootstrapState();
|
|
2440
2486
|
```
|
|
2441
2487
|
|
|
2488
|
+
### Other exports
|
|
2489
|
+
|
|
2490
|
+
| Export | Description |
|
|
2491
|
+
|---|---|
|
|
2492
|
+
| `getBindingsReady(root)` | Resolves once every binding under `root` (a `document` or a shadow root) is built; rejects if binding initialization fails |
|
|
2493
|
+
| `buildBindings(root)` | Build the bindings under a `document` or `ShadowRoot` explicitly — what the first `<wcs-state>` registered on a root schedules for it |
|
|
2494
|
+
| `getConfig()` | The current configuration (read-only view) |
|
|
2495
|
+
| `defineState(obj)` | Identity function that types `this` inside methods and getters — see [TypeScript Support](#typescript-support) |
|
|
2496
|
+
| `VERSION` | The package version; stamped into `<wcs-ssr>` and compared on hydration |
|
|
2497
|
+
| `getWcsManifest()` / `WCS_MANIFEST_VERSION` | Machine-readable manifest of the binding syntax, built-in filters and reserved names — derived from the implementation, consumed by `@wcstack/lint` and the VS Code extension |
|
|
2498
|
+
| `builtinFilterMeta` | Argument and result metadata for every built-in filter |
|
|
2499
|
+
| `analyzeContract()` | Dev-time contract analyzer; a no-op unless `enableContractAnalyzer` is on |
|
|
2500
|
+
|
|
2501
|
+
Subpath entries for tooling: `@wcstack/state/parser` (the `data-wcs` parser as a DOM-free pure function), `@wcstack/state/manifest`, and `@wcstack/state/wcs-manifest.json` (the manifest as a prebuilt JSON file).
|
|
2502
|
+
|
|
2442
2503
|
### `<wcs-state>` Element
|
|
2443
2504
|
|
|
2444
2505
|
| Attribute | Description |
|
|
@@ -2448,19 +2509,20 @@ bootstrapState();
|
|
|
2448
2509
|
| `src` | URL to `.json` or `.js` file |
|
|
2449
2510
|
| `json` | Inline JSON string |
|
|
2450
2511
|
| `bind-component` | Property name for web component binding |
|
|
2512
|
+
| `enable-ssr` | Opt into SSR: the server emits `<wcs-ssr>` hydration data for this state and the client hydrates from it instead of re-rendering — see [Server-Side Rendering](#server-side-rendering) |
|
|
2451
2513
|
|
|
2452
2514
|
### IStateElement
|
|
2453
2515
|
|
|
2454
2516
|
| Property / Method | Description |
|
|
2455
2517
|
|---|---|
|
|
2456
2518
|
| `initializePromise` | Resolves when state is fully initialized |
|
|
2519
|
+
| `connectedCallbackPromise` | Resolves once `connectedCallback` has completed (state loaded, `$connectedCallback` run) — what the testing recipes await |
|
|
2457
2520
|
| `listPaths` | Set of paths used in `for` loops |
|
|
2458
2521
|
| `getterPaths` | Set of paths defined as getters |
|
|
2459
2522
|
| `setterPaths` | Set of paths defined as setters |
|
|
2460
2523
|
| `createState(mutability, callback)` | Create a state proxy (`"readonly"` or `"writable"`) |
|
|
2461
2524
|
| `createStateAsync(mutability, callback)` | Async version of `createState` |
|
|
2462
2525
|
| `setInitialState(state)` | Set state programmatically (before initialization) |
|
|
2463
|
-
| `bindProperty(prop, descriptor)` | Define a property on the raw state object |
|
|
2464
2526
|
| `nextVersion()` | Increment and return version number |
|
|
2465
2527
|
|
|
2466
2528
|
## Architecture
|
|
@@ -2470,11 +2532,14 @@ bootstrapState()
|
|
|
2470
2532
|
└── registerComponents() // Register <wcs-state> custom element
|
|
2471
2533
|
|
|
2472
2534
|
<wcs-state> connectedCallback
|
|
2473
|
-
├──
|
|
2474
|
-
├──
|
|
2475
|
-
│
|
|
2476
|
-
│
|
|
2477
|
-
│
|
|
2535
|
+
├── one of, by placement:
|
|
2536
|
+
│ ├── _initializeDCC() // under a data-wc-definition host: define the DCC class
|
|
2537
|
+
│ ├── _initializeVolume() // mount=: graft this volume onto the root tree
|
|
2538
|
+
│ ├── _initializeBindWebComponent() // bind-component: alias the host's tree at the mount point
|
|
2539
|
+
│ └── _initialize() // root: load state (state attr / src / json / script / API)
|
|
2540
|
+
│ └── setStateElement() // Register to WeakMap<Node, IStateElement> — one tree per root
|
|
2541
|
+
│ └── (first registration per rootNode)
|
|
2542
|
+
│ └── queueMicrotask → buildBindings()
|
|
2478
2543
|
├── _callStateConnectedCallback() // Call $connectedCallback if defined
|
|
2479
2544
|
|
|
2480
2545
|
buildBindings(root)
|
|
@@ -2498,7 +2563,7 @@ Paths like `users.*.name` are decomposed into:
|
|
|
2498
2563
|
- **PathInfo** — static path metadata (segments, wildcard count, parent path)
|
|
2499
2564
|
- **ListIndex** — runtime loop index chain
|
|
2500
2565
|
- **StateAddress** — combination of PathInfo + ListIndex
|
|
2501
|
-
- **AbsoluteStateAddress** — state
|
|
2566
|
+
- **AbsolutePathInfo / AbsoluteStateAddress** — a PathInfo pinned to the state element that owns the tree, plus its ListIndex. Mounted components and volumes translate their relative paths onto the host tree at this level; v2 has one tree per root, so an address carries no state name
|
|
2502
2567
|
|
|
2503
2568
|
## Performance
|
|
2504
2569
|
|
|
@@ -2540,9 +2605,13 @@ How to read this, honestly:
|
|
|
2540
2605
|
- The heap retained after a clear is the bounded row pool that makes the next
|
|
2541
2606
|
list population cheap.
|
|
2542
2607
|
|
|
2543
|
-
Absolute numbers are from one development machine
|
|
2544
|
-
in PR#87
|
|
2545
|
-
|
|
2608
|
+
Absolute numbers are from one development machine, taken at v1.21.6 + the
|
|
2609
|
+
clear-leak fix in PR#87. The v2.0 mount work was gated on the same drivers by
|
|
2610
|
+
same-session A/B runs and stayed within run-to-run noise
|
|
2611
|
+
([docs/state-mount-impl-plan.md](../../docs/state-mount-impl-plan.md) §2-2 and
|
|
2612
|
+
slice 27), so the table has not been re-taken; absolute values swing by ±20%
|
|
2613
|
+
with machine state, and the drivers in `e2e/bench/` reproduce the comparison on
|
|
2614
|
+
your own hardware.
|
|
2546
2615
|
|
|
2547
2616
|
## Server-Side Rendering
|
|
2548
2617
|
|