@wcstack/state 4.7.0 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.ja.md +10 -9
  2. package/README.md +10 -9
  3. package/dist/auto.min.js +1 -1
  4. package/dist/auto.min.js.map +1 -1
  5. package/dist/index.esm.js +1 -1
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/parser.esm.js +1 -1
  8. package/dist/split/auto.js +1 -1
  9. package/dist/split/chunks/chunk-47I6RYV3.js +2 -0
  10. package/dist/split/chunks/{chunk-XXVJ6J2N.js.map → chunk-47I6RYV3.js.map} +1 -1
  11. package/dist/split/chunks/{chunk-DRSAQE4E.js → chunk-EBIQH2HE.js} +2 -2
  12. package/dist/split/chunks/{chunk-P3EXTBXC.js → chunk-FQUDP3XK.js} +2 -2
  13. package/dist/split/chunks/{chunk-IBGHHABS.js → chunk-ISBASPV2.js} +2 -2
  14. package/dist/split/chunks/{chunk-GQTEYSRZ.js → chunk-J3PTKUWV.js} +2 -2
  15. package/dist/split/chunks/{chunk-QMOIMPIJ.js → chunk-LWRYFYOC.js} +2 -2
  16. package/dist/split/chunks/{chunk-PKEAYPFR.js → chunk-P3JKSHRN.js} +2 -2
  17. package/dist/split/chunks/{chunk-T3KW5DZD.js → chunk-SOFOV2OG.js} +2 -2
  18. package/dist/split/chunks/{chunk-NITPUCPA.js → chunk-TS2HEHBQ.js} +2 -2
  19. package/dist/split/chunks/chunk-YIDHPXYQ.js +2 -0
  20. package/dist/split/chunks/{chunk-UN6ADZKP.js.map → chunk-YIDHPXYQ.js.map} +1 -1
  21. package/dist/split/core.js +1 -1
  22. package/dist/split/features/devtools.js +1 -1
  23. package/dist/split/features/diagnostics.js +1 -1
  24. package/dist/split/features/diagnostics.js.map +1 -1
  25. package/dist/split/features/formats.js +1 -1
  26. package/dist/split/features/list-keys.js +1 -1
  27. package/dist/split/features/native-commands.js +1 -1
  28. package/dist/split/features/recursion.js +1 -1
  29. package/dist/split/features/scopes.js +1 -1
  30. package/dist/split/features/ssr.js +1 -1
  31. package/dist/split/features/temporal.js +1 -1
  32. package/package.json +1 -1
  33. package/dist/split/chunks/chunk-UN6ADZKP.js +0 -2
  34. package/dist/split/chunks/chunk-XXVJ6J2N.js +0 -2
  35. /package/dist/split/chunks/{chunk-DRSAQE4E.js.map → chunk-EBIQH2HE.js.map} +0 -0
  36. /package/dist/split/chunks/{chunk-P3EXTBXC.js.map → chunk-FQUDP3XK.js.map} +0 -0
  37. /package/dist/split/chunks/{chunk-IBGHHABS.js.map → chunk-ISBASPV2.js.map} +0 -0
  38. /package/dist/split/chunks/{chunk-GQTEYSRZ.js.map → chunk-J3PTKUWV.js.map} +0 -0
  39. /package/dist/split/chunks/{chunk-QMOIMPIJ.js.map → chunk-LWRYFYOC.js.map} +0 -0
  40. /package/dist/split/chunks/{chunk-PKEAYPFR.js.map → chunk-P3JKSHRN.js.map} +0 -0
  41. /package/dist/split/chunks/{chunk-T3KW5DZD.js.map → chunk-SOFOV2OG.js.map} +0 -0
  42. /package/dist/split/chunks/{chunk-NITPUCPA.js.map → chunk-TS2HEHBQ.js.map} +0 -0
package/README.ja.md CHANGED
@@ -193,9 +193,9 @@
193
193
  <script type="importmap">
194
194
  {
195
195
  "imports": {
196
- "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/core.js",
197
- "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/temporal.js",
198
- "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/scopes.js"
196
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/core.js",
197
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/features/temporal.js",
198
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/features/scopes.js"
199
199
  }
200
200
  }
201
201
  </script>
@@ -261,7 +261,7 @@
261
261
  ページが名指したアドオンを、同じビルドから読み込みます。
262
262
 
263
263
  ```html
264
- <script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/auto.js"></script>
264
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/auto.js"></script>
265
265
  <wcs-state features="scopes diagnostics">
266
266
  <script type="module">
267
267
  export default {
@@ -1235,7 +1235,7 @@ getter の例外は握り潰されません。評価された場所(バイン
1235
1235
  | **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
1236
1236
  | **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`$behavior.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外)。一覧は例外で、行が配列の要素に合わせ直されるのは**新しい**配列を代入したときだけ。同じ配列の再代入や `$postUpdate` は、ある行の下を読み直すが、行を追加・削除・置き換えしない: `cards[i] = card; this.cards = cards` は古いカードを出し続ける。新しい配列を代入する(`this.cards = cards.with(i, card)`、`[...cards, x]`)。lint はその場の変更を報告する(`wcs/array-index-assign`、`wcs/array-mutation`) |
1237
1237
 
1238
- 規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。
1238
+ 規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。4.9 から、getter が読むパスをページの束縛が書くとき — `<wcs-fetch data-wcs="...: monthFetch">` は `monthFetch.value` を書くので、`this.monthFetch.value` を読む getter は応答を見ない — は **error** で、スクリプトだけが書くときは warning のままです。
1239
1239
 
1240
1240
  `$untracked(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$dependOn(path)` は最初の規則に対する逃げ道です。
1241
1241
 
@@ -2346,6 +2346,7 @@ $on: {
2346
2346
  - `$on` のすべてのキーは `$eventTokens` で宣言済みであること(さもなくば初期化時に throw)
2347
2347
  - 各値は関数であること
2348
2348
  - シグネチャは `(state, event, ...listIndexes)` —— まず DOM の `Event`、続いて内包するループインデックス
2349
+ - ハンドラの中の `this` は undefined なので、`this` を使うハンドラはイベントが届いたときに throw します。4.9 から、`diagnostics` の後付けが、state の宣言のときにハンドラごとに 1 回、イベントより前にそう言います(`[wcs/on-handler-this]`)。lint は error として報告します
2349
2350
 
2350
2351
  ### `eventToken.<property>:` バインディング
2351
2352
 
@@ -2862,7 +2863,7 @@ export default {
2862
2863
  `$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
2863
2864
 
2864
2865
  - フック内の `this` は読み書き可能な状態プロキシです。
2865
- - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
2866
+ - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。これは要素の接続で、ページのものではありません: ページがスリープから復帰したとき、凍結から再開したとき、back/forward cache から戻ったとき、可視になったときには走りません — [開いたままのページ](../../docs/long-lived-pages.ja.md) を見てください。
2866
2867
  - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
2867
2868
  - `$renderedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのバインディングは、したがってその私有キーも、ホストの `paths` には決して現れません。`async` を使用できますが、戻り値は await されません。throw または reject した場合は `console.error` で報告されます。
2868
2869
  - `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま。`for:` で描画していないリスト —— `$getAll` か `$watch` だけが保持しているリスト —— では `node` は `null`)。`for:` / `if:` の行の中で取り付け時に拒否されたバインディング(未宣言の token、要素が宣言していないメンバ)も、そのバインディングの失敗としてここに報告され、行はそのまま構築されます。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
@@ -2953,7 +2954,7 @@ li {
2953
2954
  | 26 | | `filter <name> requires at least one option` |
2954
2955
  | 27 | | `filter <name> requires a number as option` |
2955
2956
  | 28 | | `filter <name> requires a number value, not <what it was>` (a string adds: `— put |number before |<name>, or convert it where it is read`) |
2956
- | 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`) |
2957
+ | 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`。4.9 から、`"2026-10-01"` のような日付だけの文字列には、`new Date("2026-10-01")` は UTC の真夜中 — グリニッジより西では前の日 — で、`new Date(y, m - 1, d)` がページのある場所のその日だ、とも言う) |
2957
2958
  | 30 | | `filter <name> requires an array value` |
2958
2959
  | 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
2959
2960
  | 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
@@ -3109,7 +3110,7 @@ this.$getAll("matrix.*.*", [row]);
3109
3110
  - 読めないソース([状態の初期化](#状態の初期化))。
3110
3111
  - 不正な宣言: 削除された名前(`$scan`・`$streams`・`$updatedCallback`。`diagnostics` アドオンがあるとき)、形の誤った `$behavior` / `$features` / `$commandTokens` / `$eventTokens` / `$on`、アドオンが入っていない宣言(`[wcs/feature-not-installed]`)。
3111
3112
  - ページのバインド中に見つかったマークアップの誤り: 構文エラー、未知のフィルタや誤った引数の個数、wildcard-rank のエラー、フィルタ付きの `for:`、テンプレートの中の `outerHTML:`、クラスが定義済みの要素での wc-bindable の設定ミス。走査はそのエラーで止まります: 文書順でそれより前はバインドされ、後はバインドされません。デプロイの前に `npx @wcstack/lint` を実行してください。
3112
- - 別の `<wcs-state>` がすでにバインドしているルートの上の、2 つ目のルート `<wcs-state>`(`#47`)。state ツリーはルートごとに 1 本です。部分木は `mount=` で接ぎ木してください。
3113
+ - 別の `<wcs-state>` がすでにバインドしているルートの上の、2 つ目のルート `<wcs-state>`(`#47`)。state ツリーはルートごとに 1 本です。部分木は `mount=` で接ぎ木してください。バインドするのは、文書で先のものではなく、状態が先にそろったものです: `json=` の state はすぐにそろうので、文書で先の `src=` やインラインの state より先にルートを取り、拒まれるのは後者になり、ページは違う state でバインドされます。4.9 から、`diagnostics` の後付けが、先にバインドしたときに両方を名指しします(`[wcs/second-root]`)。
3113
3114
 
3114
3115
  `for:` / `if:` の行の中で、バインディングの取り付け時に見つかったエラー(未宣言の token、要素が宣言していないメンバ)は、初期化の失敗ではなく、そのバインディングの失敗になります: `$errorCallback` に届き、行はそのまま構築されます。state にないトップレベルのキーへのバインディングは、初期化の失敗ではなく適用の失敗です。初期化に失敗した要素は再始動できません —— `setInitialState()` は throw します(`#14`)。取り除いて新しい要素を作ってください。
3115
3116
 
@@ -3336,7 +3337,7 @@ router 自身が `<html lang>` を読む案と、リンク単位の intercept
3336
3337
 
3337
3338
  `<wcs-state>` で組んだページは素の DOM なので、[happy-dom](https://github.com/capricorn86/happy-dom) でヘッドレスにテストできます — ブラウザ不要・ビルド不要・テスト専用 API 不要。レシピは 3 つ、いずれも書いてあるとおりに動きます。
3338
3339
 
3339
- 1 import で済ませたいなら [`@wcstack/testing`](../testing/README.ja.md) がレシピ 1 を `mount()` / `settle()` / `fire()` にまとめています(`<wcs-router>` も待ちます)。以下の素のレシピはそれ無しでも有効です。
3340
+ 1 import で済ませたいなら [`@wcstack/testing`](../testing/README.ja.md) がレシピ 1 を `mount()` / `settle()` / `fire()` にまとめています(`<wcs-router>` も待ちます)。以下の素のレシピはそれ無しでも有効です。自分のファイルにある状態(`<wcs-state src="./state.js">`)は、4.9 から `mount()` の `resolveModule` オプションで読み込めます — happy-dom の下では `src` は `http:` の URL で、Node のモジュールローダはそれを読みません。インラインの状態は、Node の下では相対モジュールを `import` できません。
3340
3341
 
3341
3342
  ### 1. vitest + happy-dom
3342
3343
 
package/README.md CHANGED
@@ -194,9 +194,9 @@ leaves features out, it can compose them instead:
194
194
  <script type="importmap">
195
195
  {
196
196
  "imports": {
197
- "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/core.js",
198
- "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/temporal.js",
199
- "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/scopes.js"
197
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/core.js",
198
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/features/temporal.js",
199
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/features/scopes.js"
200
200
  }
201
201
  }
202
202
  </script>
@@ -263,7 +263,7 @@ receptacle the owning add-on fills on install).
263
263
  core, and the add-ons the page names from the same build.
264
264
 
265
265
  ```html
266
- <script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/auto.js"></script>
266
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.9.0/dist/split/auto.js"></script>
267
267
  <wcs-state features="scopes diagnostics">
268
268
  <script type="module">
269
269
  export default {
@@ -1243,7 +1243,7 @@ Three rules decide what the dependency graph sees. None of them matters until yo
1243
1243
  | **Reads inside a setter are not tracked.** A setter is an imperative assignment, not a derivation, so nothing it reads becomes a dependency of anything | A setter that reads `this.a` to decide what to write does not run again when `a` changes — only a getter re-runs |
1244
1244
  | **The same-value guard applies to primitives only.** A primitive write `Object.is`-equal to the current value is dropped before anything is enqueued; an object or array write always passes, even the same reference | Assigning the same string again fires nothing; assigning the same object again re-fires its bindings and `$watch` (`$behavior.sameValueGuard`; a `semantics: "event"` property is exempt either way). A list is the exception: its rows follow the array's items only when a **new** array is assigned. The same array assigned again — or `$postUpdate` — re-reads what is below the rows it has, but adds, removes or replaces no row: `cards[i] = card; this.cards = cards` keeps showing the old card. Assign a new array (`this.cards = cards.with(i, card)`, `[...cards, x]`); lint reports the in-place change (`wcs/array-index-assign`, `wcs/array-mutation`) |
1245
1245
 
1246
- The first rule is the one static analysis can catch: `wcs-validate` and the VS Code extension report `wcs/getter-untracked-read` when a getter reads `this.form.name` and the document writes `form.name` somewhere (a `value:` binding, a spread, `this["form.name"] = …`). A root that is only ever replaced wholesale — router params, a `$stream` fold — is left alone.
1246
+ The first rule is the one static analysis can catch: `wcs-validate` and the VS Code extension report `wcs/getter-untracked-read` when a getter reads `this.form.name` and the document writes `form.name` somewhere (a `value:` binding, a spread, `this["form.name"] = …`). A root that is only ever replaced wholesale — router params, a `$stream` fold — is left alone. Since 4.9 it is an **error** when a binding on the page writes the path the getter reads — `<wcs-fetch data-wcs="...: monthFetch">` writes `monthFetch.value`, so a getter reading `this.monthFetch.value` never sees the response — and stays a warning when only the script writes it.
1247
1247
 
1248
1248
  `$untracked(fn)` applies the setter rule to a getter on purpose: reads inside `fn` are not tracked. `$dependOn(path)` is the escape hatch for the first rule.
1249
1249
 
@@ -2358,6 +2358,7 @@ $on: {
2358
2358
  - Every `$on` key must be declared in `$eventTokens` (otherwise an error is thrown at initialization)
2359
2359
  - Each value must be a function
2360
2360
  - The signature is `(state, event, ...listIndexes)` — the DOM `Event` first, then any enclosing loop indexes
2361
+ - `this` is undefined in a handler, so a handler that uses it throws when the event arrives. Since 4.9 the `diagnostics` add-on says so once per handler when the state is declared, before any event (`[wcs/on-handler-this]`); lint reports it as an error
2361
2362
 
2362
2363
  ### `eventToken.<property>:` Binding
2363
2364
 
@@ -2874,7 +2875,7 @@ State objects can define `$connectedCallback`, `$disconnectedCallback`, `$render
2874
2875
  All hooks except `$disconnectedCallback` support `async` — you can use `async/await` in any of them. Since the reactive proxy detects every property assignment as a change, standard `async/await` with direct property updates is sufficient for asynchronous operations — loading flags, fetched data, and error messages are all just property assignments, without requiring additional abstractions for async state management.
2875
2876
 
2876
2877
  - `this` inside hooks is the state proxy with full read/write access
2877
- - `$connectedCallback` is called **every time** the element is connected (including re-insertion after removal), making it suitable for setup that should be re-established
2878
+ - `$connectedCallback` is called **every time** the element is connected (including re-insertion after removal), making it suitable for setup that should be re-established. It is the element's connection, not the page's: it does not run when the page wakes from sleep, resumes from a freeze, comes back from the back/forward cache or becomes visible — see [Pages that stay open](../../docs/long-lived-pages.md)
2878
2879
  - `$disconnectedCallback` is called synchronously — use it for cleanup such as clearing timers, removing event listeners, or releasing resources
2879
2880
  - `$renderedCallback(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. A mounted component's bindings, and so its private keys, never appear in the host's `paths`. Can be `async`, but the return value is not awaited; one that throws or rejects is reported with `console.error`
2880
2881
  - `$errorCallback(error, info)` is the in-page **error boundary** for bindings. When applying a binding throws — a path getter or filter threw, a structural directive failed — the failure is isolated (the rest of the batch still applies, and neither the value nor the DOM is rolled back) and, without this hook, reported with `console.error`. Declare the hook and the report comes to you instead: `error` is what was thrown, `info` is `{ path, bindingType, node }` identifying the binding (`path` as written in `data-wcs`, wildcards intact; `node` is `null` for a list that no `for:` renders — one only `$getAll` or `$watch` keeps). A binding refused as it is attached inside a `for:` / `if:` row (an undeclared token, a member the element does not declare) is reported here too, as that binding's failure, and the row is still built. `this` is the writable state proxy, so the usual shape is to write the message into state and render it like anything else:
@@ -2965,7 +2966,7 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
2965
2966
  | 26 | | `filter <name> requires at least one option` |
2966
2967
  | 27 | | `filter <name> requires a number as option` |
2967
2968
  | 28 | | `filter <name> requires a number value, not <what it was>` (a string adds: `— put |number before |<name>, or convert it where it is read`) |
2968
- | 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`) |
2969
+ | 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`; since 4.9, a date-only string such as `"2026-10-01"` also says that `new Date("2026-10-01")` is midnight UTC — the day before west of Greenwich — and `new Date(y, m - 1, d)` is that day where the page is) |
2969
2970
  | 30 | | `filter <name> requires an array value` |
2970
2971
  | 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
2971
2972
  | 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
@@ -3122,7 +3123,7 @@ What fails initialization:
3122
3123
  - A source that cannot be read ([State Initialization](#state-initialization)).
3123
3124
  - An invalid declaration: a removed name (`$scan`, `$streams`, `$updatedCallback`; with the `diagnostics` add-on), a `$behavior` / `$features` / `$commandTokens` / `$eventTokens` / `$on` of the wrong shape, a declaration whose add-on is not installed (`[wcs/feature-not-installed]`).
3124
3125
  - A markup error found while the page is bound: a syntax error, an unknown filter or a wrong argument count, a wildcard-rank error, `for:` with filters, `outerHTML:` in a template, a wc-bindable misconfiguration on an element whose class is already defined. The walk stops at the error: what comes before it in document order is bound, what comes after it is not. Run `npx @wcstack/lint` before you deploy.
3125
- - A second root `<wcs-state>` on a root that another one already binds (`#47`). There is one state tree per root; graft a subtree with `mount=`.
3126
+ - A second root `<wcs-state>` on a root that another one already binds (`#47`). There is one state tree per root; graft a subtree with `mount=`. The one that binds is the one whose state is ready first, not the first in the document: a `json=` state is ready at once, so it takes the root before an earlier `src=` or inline state, which is then the one refused, and the page binds against the wrong state. Since 4.9 the `diagnostics` add-on names both when the first binds (`[wcs/second-root]`).
3126
3127
 
3127
3128
  Inside a `for:` / `if:` row, an error found while a binding is attached (an undeclared token, a member the element does not declare) is that binding's failure instead: it goes to `$errorCallback`, and the row is still built. A binding to a top-level key the state does not have is a failed apply, not an initialization failure. An element that failed to initialize cannot be re-armed — `setInitialState()` throws (`#14`); remove it and create a new one.
3128
3129
 
@@ -3353,7 +3354,7 @@ different shape; `examples/router-i18n` is the reference layout.
3353
3354
 
3354
3355
  A page built on `<wcs-state>` is plain DOM, so it can be tested headlessly with [happy-dom](https://github.com/capricorn86/happy-dom) — no browser, no build step, no test-only API. Three recipes follow; every one of them runs as written.
3355
3356
 
3356
- Want it as one import? [`@wcstack/testing`](../testing/README.md) packages recipe 1 as `mount()` / `settle()` / `fire()` (and waits for `<wcs-router>` too). The bare recipes below stay valid without it.
3357
+ Want it as one import? [`@wcstack/testing`](../testing/README.md) packages recipe 1 as `mount()` / `settle()` / `fire()` (and waits for `<wcs-router>` too). The bare recipes below stay valid without it. A state in its own file (`<wcs-state src="./state.js">`) loads under `mount()` with its `resolveModule` option, since 4.9 — under happy-dom the `src` is an `http:` URL, which Node's module loader does not read, and an inline state cannot `import` a relative module under Node.
3357
3358
 
3358
3359
  ### 1. vitest + happy-dom
3359
3360