@wcstack/state 1.31.0 → 1.32.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.
- package/README.ja.md +124 -7
- package/README.md +126 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.esm.js +970 -108
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +28 -8
- package/dist/parser.esm.js +28 -8
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
3
|
+
// Generated from /protocol/ssr-snapshot.ts by scripts/sync-protocol-types.mjs.
|
|
4
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
5
|
+
// ===========================================================================
|
|
6
|
+
// ssr-snapshot protocol — how the SSR renderer asks whoever owns reactive
|
|
7
|
+
// state to build hydration snapshots (<wcs-ssr>) as a final pass, after every
|
|
8
|
+
// DOM inserter (router route content, late custom elements) has settled.
|
|
9
|
+
//
|
|
10
|
+
// Without this, the snapshot is built inside <wcs-state>'s connectedCallback
|
|
11
|
+
// and races DOM inserted by other packages: whether a route's structural
|
|
12
|
+
// templates make it into the snapshot depends on document order and state's
|
|
13
|
+
// load mechanism (docs/ssr-router-design.md §5).
|
|
14
|
+
//
|
|
15
|
+
// The provider (@wcstack/state) installs itself on a well-known global symbol
|
|
16
|
+
// at bootstrap. The renderer (@wcstack/server) looks the builder up after
|
|
17
|
+
// running bootstraps: if present it announces orchestration by setting
|
|
18
|
+
// `data-wcs-server="orchestrated"` on the document element BEFORE parsing, and
|
|
19
|
+
// calls build() right before serialization. The provider keeps its inline
|
|
20
|
+
// per-element fallback whenever the attribute value is anything else, so:
|
|
21
|
+
// - old renderer + new provider -> inline build, yesterday's behavior
|
|
22
|
+
// - new renderer + old provider -> no builder found, attribute stays "",
|
|
23
|
+
// the old provider builds inline as before
|
|
24
|
+
// - new renderer + new provider -> orchestrated: snapshots are built last
|
|
25
|
+
// and therefore always see settled DOM
|
|
26
|
+
//
|
|
27
|
+
// The symbol (rather than a package import) also pins the builder to the state
|
|
28
|
+
// copy that actually runs on the page — its module-scoped fragment registries
|
|
29
|
+
// are the ones the snapshot must read.
|
|
30
|
+
//
|
|
31
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/ssr-snapshot.ts), then
|
|
32
|
+
// run `node scripts/sync-protocol-types.mjs` to regenerate the per-package
|
|
33
|
+
// copies (packages/<pkg>/src/protocol/ssrSnapshot.ts). Those copies are
|
|
34
|
+
// generated — do not edit them.
|
|
35
|
+
/**
|
|
36
|
+
* Global key the snapshot builder installs itself under. `Symbol.for` so
|
|
37
|
+
* independently loaded copies of this file (state's and server's) still agree.
|
|
38
|
+
*/
|
|
39
|
+
const SSR_SNAPSHOT_BUILDER_KEY = Symbol.for("wcstack.ssr.snapshotBuilder");
|
|
40
|
+
/**
|
|
41
|
+
* `data-wcs-server` attribute value announcing that the renderer will call the
|
|
42
|
+
* builder as a final pass. Providers must skip their inline per-element build
|
|
43
|
+
* when they see this value, and keep it for any other value (including "").
|
|
44
|
+
*/
|
|
45
|
+
const SSR_ORCHESTRATED_VALUE = "orchestrated";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* サーバー主導スナップショット(orchestrated)の判定
|
|
49
|
+
* (docs/ssr-router-design.md §5)。renderToString が snapshot builder を
|
|
50
|
+
* 見つけたときだけ `data-wcs-server="orchestrated"` を宣言する — 値が他の
|
|
51
|
+
* もの(旧 server の "" を含む)なら inline 生成が従来どおり働く。
|
|
52
|
+
* inSsr と同じ理由でキャッシュしない。
|
|
53
|
+
*/
|
|
54
|
+
function isOrchestratedSsr() {
|
|
55
|
+
const html = document.documentElement;
|
|
56
|
+
return html ? html.getAttribute('data-wcs-server') === SSR_ORCHESTRATED_VALUE : false;
|
|
57
|
+
}
|
|
1
58
|
function inSsr() {
|
|
2
59
|
// キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
|
|
3
60
|
// 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
|
|
@@ -1142,16 +1199,33 @@ const fix = (options) => {
|
|
|
1142
1199
|
/**
|
|
1143
1200
|
* Locale number filter - formats number according to locale.
|
|
1144
1201
|
*
|
|
1202
|
+
* ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は
|
|
1203
|
+
* **明示引数だけを構築時に確定し、既定の `config.locale` は適用のたびに読む**。
|
|
1204
|
+
*
|
|
1205
|
+
* 以前は `options?.[0] ?? config.locale` を返り値の関数の**外**で解決していた。
|
|
1206
|
+
* フィルタ関数はバインド構築時に一度だけ作られるので、これはロケールを
|
|
1207
|
+
* クロージャに焼き込むことを意味する。`config.locale` の確定がバインド構築より
|
|
1208
|
+
* 遅れると、それ以降どう直しても「同じページの中で日付だけ既定ロケール」が
|
|
1209
|
+
* 永続し、しかも `config.locale` は依存グラフに載らないので再描画で回復もしない。
|
|
1210
|
+
* 症状(日付だけ英語)は原因(起動順序)から遠く、追いにくい。
|
|
1211
|
+
*
|
|
1212
|
+
* 適用のたびに読めば、少なくとも**再適用されたバインドは回復する**。ロケールは
|
|
1213
|
+
* 起動時に確定する前提(docs/i18n-design.md D1)なので通常この差は現れず、
|
|
1214
|
+
* これは順序事故から復帰できるようにするための保険である。
|
|
1215
|
+
*
|
|
1216
|
+
* 明示引数(`|date(ja-JP)`)は構築時に固定でよい — バインド式の一部であり、
|
|
1217
|
+
* 実行中に変わらない。
|
|
1218
|
+
*
|
|
1145
1219
|
* @param options - Array with locale string as first element (default: config.locale)
|
|
1146
1220
|
* @returns Filter function that returns localized number string
|
|
1147
1221
|
*/
|
|
1148
1222
|
const locale = (options) => {
|
|
1149
|
-
const
|
|
1223
|
+
const explicit = options?.[0];
|
|
1150
1224
|
return (value) => {
|
|
1151
1225
|
if (typeof value !== 'number') {
|
|
1152
1226
|
valueMustBeNumber('locale');
|
|
1153
1227
|
}
|
|
1154
|
-
return value.toLocaleString(
|
|
1228
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1155
1229
|
};
|
|
1156
1230
|
};
|
|
1157
1231
|
/**
|
|
@@ -1464,12 +1538,13 @@ const truncate = (options) => {
|
|
|
1464
1538
|
* @returns Filter function that returns date string
|
|
1465
1539
|
*/
|
|
1466
1540
|
const date = (options) => {
|
|
1467
|
-
|
|
1541
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1542
|
+
const explicit = options?.[0];
|
|
1468
1543
|
return (value) => {
|
|
1469
1544
|
if (!(value instanceof Date)) {
|
|
1470
1545
|
valueMustBeDate('date');
|
|
1471
1546
|
}
|
|
1472
|
-
return value.toLocaleDateString(
|
|
1547
|
+
return value.toLocaleDateString(explicit ?? config.locale);
|
|
1473
1548
|
};
|
|
1474
1549
|
};
|
|
1475
1550
|
/**
|
|
@@ -1479,12 +1554,13 @@ const date = (options) => {
|
|
|
1479
1554
|
* @returns Filter function that returns time string
|
|
1480
1555
|
*/
|
|
1481
1556
|
const time = (options) => {
|
|
1482
|
-
|
|
1557
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1558
|
+
const explicit = options?.[0];
|
|
1483
1559
|
return (value) => {
|
|
1484
1560
|
if (!(value instanceof Date)) {
|
|
1485
1561
|
valueMustBeDate('time');
|
|
1486
1562
|
}
|
|
1487
|
-
return value.toLocaleTimeString(
|
|
1563
|
+
return value.toLocaleTimeString(explicit ?? config.locale);
|
|
1488
1564
|
};
|
|
1489
1565
|
};
|
|
1490
1566
|
/**
|
|
@@ -1494,12 +1570,13 @@ const time = (options) => {
|
|
|
1494
1570
|
* @returns Filter function that returns datetime string
|
|
1495
1571
|
*/
|
|
1496
1572
|
const datetime = (options) => {
|
|
1497
|
-
|
|
1573
|
+
// 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
|
|
1574
|
+
const explicit = options?.[0];
|
|
1498
1575
|
return (value) => {
|
|
1499
1576
|
if (!(value instanceof Date)) {
|
|
1500
1577
|
valueMustBeDate('datetime');
|
|
1501
1578
|
}
|
|
1502
|
-
return value.toLocaleString(
|
|
1579
|
+
return value.toLocaleString(explicit ?? config.locale);
|
|
1503
1580
|
};
|
|
1504
1581
|
};
|
|
1505
1582
|
/**
|
|
@@ -4568,6 +4645,16 @@ function addInterestedSession(node, session) {
|
|
|
4568
4645
|
}
|
|
4569
4646
|
interestedSessionsByNode.set(node, new Set([current, session]));
|
|
4570
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* このノードに既にバインドが張られているか。
|
|
4650
|
+
*
|
|
4651
|
+
* binder プロトコル(`bind()`)の冪等判定に使う。`remember` が binding ごとに
|
|
4652
|
+
* `addInterestedSession(binding.replaceNode, …)` を呼ぶので、バインド済みノードは
|
|
4653
|
+
* 必ずこの台帳に載っている。新しい台帳を足さずに済むぶん、二重管理の齟齬が無い。
|
|
4654
|
+
*/
|
|
4655
|
+
function hasInterestedSession(node) {
|
|
4656
|
+
return interestedSessionsByNode.has(node);
|
|
4657
|
+
}
|
|
4571
4658
|
function forEachInterestedSession(node, callback) {
|
|
4572
4659
|
const current = interestedSessionsByNode.get(node);
|
|
4573
4660
|
if (typeof current === "undefined")
|
|
@@ -6452,8 +6539,23 @@ class Content {
|
|
|
6452
6539
|
let anchor = targetNode;
|
|
6453
6540
|
for (const node of this._movableNodes()) {
|
|
6454
6541
|
if (anchor.nextSibling !== node) {
|
|
6542
|
+
// moveBefore も childList mutation record を出すため、マークは両分岐の前
|
|
6455
6543
|
markObserverSkipOnAdd(node);
|
|
6456
|
-
|
|
6544
|
+
// moveBefore は取り外しを伴わない移動 — 接続済み行の reorder で
|
|
6545
|
+
// フォーカス・iframe・アニメーション状態を保存する(docs/a11y-design.md §4-1)。
|
|
6546
|
+
// この 1 文は 4 つのノード状態を共有する: (a) 接続済み reorder、
|
|
6547
|
+
// (b) clone フラグメント由来(root 違い)、(c) プール/unmount 済み(親なし)、
|
|
6548
|
+
// (d) バッチフラグメント内。moveBefore は「同一ツリー・親あり」を要求し
|
|
6549
|
+
// (b)(c)(d) では HierarchyRequestError を投げるため、same-parent ガード
|
|
6550
|
+
// (同 root かつ親が非 null の同時証明 = フォーカス保存が意味を持つ (a) と
|
|
6551
|
+
// 正確に一致)は外せない。ガードを外す「簡略化」をしてはならない。
|
|
6552
|
+
const mover = parentNode;
|
|
6553
|
+
if (node.parentNode === parentNode && typeof mover.moveBefore === "function") {
|
|
6554
|
+
mover.moveBefore(node, anchor.nextSibling);
|
|
6555
|
+
}
|
|
6556
|
+
else {
|
|
6557
|
+
parentNode.insertBefore(node, anchor.nextSibling);
|
|
6558
|
+
}
|
|
6457
6559
|
}
|
|
6458
6560
|
anchor = node;
|
|
6459
6561
|
}
|
|
@@ -6665,6 +6767,157 @@ function createContent(bindingInfo) {
|
|
|
6665
6767
|
return content;
|
|
6666
6768
|
}
|
|
6667
6769
|
|
|
6770
|
+
// ===========================================================================
|
|
6771
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
6772
|
+
// Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
|
|
6773
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
6774
|
+
// ===========================================================================
|
|
6775
|
+
// transition-runner protocol — how a package that mutates the DOM hands that
|
|
6776
|
+
// mutation to whoever is arbitrating view transitions on the page.
|
|
6777
|
+
//
|
|
6778
|
+
// @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
|
|
6779
|
+
// (zero runtime dependencies, independently publishable), so the arbiter installs
|
|
6780
|
+
// itself on a well-known global symbol and the participants look it up lazily.
|
|
6781
|
+
// No arbiter installed means the mutation is invoked directly, synchronously —
|
|
6782
|
+
// byte-for-byte the behavior these packages had before the protocol existed.
|
|
6783
|
+
//
|
|
6784
|
+
// docs/view-transition-design.md §4 is the normative description.
|
|
6785
|
+
//
|
|
6786
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
|
|
6787
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
6788
|
+
// (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
|
|
6789
|
+
/**
|
|
6790
|
+
* Global key the arbiter installs itself under. `Symbol.for` so independently
|
|
6791
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
6792
|
+
*/
|
|
6793
|
+
const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
|
|
6794
|
+
/**
|
|
6795
|
+
* The installed arbiter, or null when there is none, it speaks a version this
|
|
6796
|
+
* reader does not, or it does not accept this participant.
|
|
6797
|
+
*
|
|
6798
|
+
* Looked up on every call rather than cached: the tag can be added, removed, or
|
|
6799
|
+
* reconfigured at any point in a page's life, and a stale cache would either
|
|
6800
|
+
* animate what the author just switched off or miss what they switched on.
|
|
6801
|
+
*/
|
|
6802
|
+
function getTransitionRunner(source) {
|
|
6803
|
+
const candidate = globalThis[TRANSITION_RUNNER_KEY];
|
|
6804
|
+
if (candidate === undefined || candidate === null)
|
|
6805
|
+
return null;
|
|
6806
|
+
if (candidate.protocol !== "wcs-transition-runner")
|
|
6807
|
+
return null;
|
|
6808
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
6809
|
+
return null;
|
|
6810
|
+
if (typeof candidate.run !== "function")
|
|
6811
|
+
return null;
|
|
6812
|
+
if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
|
|
6813
|
+
return null;
|
|
6814
|
+
return candidate;
|
|
6815
|
+
}
|
|
6816
|
+
/**
|
|
6817
|
+
* Run `mutate` under the installed arbiter, or directly when there is none.
|
|
6818
|
+
*
|
|
6819
|
+
* Returns `undefined` in the no-arbiter case instead of a resolved promise: the
|
|
6820
|
+
* state drain calls this on every batch, and awaiting is a caller's choice, not
|
|
6821
|
+
* an allocation the common path should pay for. `await` accepts both.
|
|
6822
|
+
*/
|
|
6823
|
+
function runTransition(source, mutate, types) {
|
|
6824
|
+
const runner = getTransitionRunner(source);
|
|
6825
|
+
if (runner === null) {
|
|
6826
|
+
mutate();
|
|
6827
|
+
return undefined;
|
|
6828
|
+
}
|
|
6829
|
+
return runner.run(mutate, { source, types });
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
/** Elements that already carry a generated name (never renamed). */
|
|
6833
|
+
const namedElements = new WeakSet();
|
|
6834
|
+
/**
|
|
6835
|
+
* The generated-name ledger is per *document*, not per module instance.
|
|
6836
|
+
*
|
|
6837
|
+
* `view-transition-name` has to be unique across the whole document: the moment
|
|
6838
|
+
* two elements share one, the browser aborts the transition outright. A
|
|
6839
|
+
* module-scope counter breaks that as soon as `@wcstack/state` is loaded twice on
|
|
6840
|
+
* one page (two CDN bundles), because both copies would start minting
|
|
6841
|
+
* `wcs-row-1`. The transition-runner key is a `Symbol.for` for exactly this
|
|
6842
|
+
* reason, and the counter needs the same protection.
|
|
6843
|
+
*
|
|
6844
|
+
* Sharing the cap is right for the same reason: the cost a cap exists to bound —
|
|
6845
|
+
* one snapshot group per named element — is a document-wide cost, not a
|
|
6846
|
+
* per-bundle one.
|
|
6847
|
+
*/
|
|
6848
|
+
const NAMING_LEDGER_KEY = Symbol.for("wcstack.state.view-transition-naming");
|
|
6849
|
+
function getLedger() {
|
|
6850
|
+
const slot = globalThis;
|
|
6851
|
+
return (slot[NAMING_LEDGER_KEY] ??= { counter: 0, assigned: 0, warned: false });
|
|
6852
|
+
}
|
|
6853
|
+
/**
|
|
6854
|
+
* The active auto-naming policy, or null when names are the author's business
|
|
6855
|
+
* (the default) — one arbiter lookup per structural apply, not per row.
|
|
6856
|
+
*/
|
|
6857
|
+
function getAutoNaming() {
|
|
6858
|
+
const runner = getTransitionRunner("state");
|
|
6859
|
+
if (runner === null || runner.naming !== "auto") {
|
|
6860
|
+
return null;
|
|
6861
|
+
}
|
|
6862
|
+
return { limit: runner.namingLimit };
|
|
6863
|
+
}
|
|
6864
|
+
function firstElementOf(content) {
|
|
6865
|
+
const first = content.firstNode;
|
|
6866
|
+
if (first === null) {
|
|
6867
|
+
return null;
|
|
6868
|
+
}
|
|
6869
|
+
const last = content.lastNode;
|
|
6870
|
+
for (let node = first; node !== null; node = node.nextSibling) {
|
|
6871
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6872
|
+
return node;
|
|
6873
|
+
}
|
|
6874
|
+
if (node === last) {
|
|
6875
|
+
break;
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return null;
|
|
6879
|
+
}
|
|
6880
|
+
/**
|
|
6881
|
+
* Give this content's first element a unique name plus a class for group
|
|
6882
|
+
* styling, unless it already has one or the cap has been reached.
|
|
6883
|
+
*
|
|
6884
|
+
* The cap exists because every named element becomes its own snapshot group; a
|
|
6885
|
+
* few hundred of them make a transition visibly slow. Past it naming stops and
|
|
6886
|
+
* says so once — silently degrading would leave the author wondering why only
|
|
6887
|
+
* the first part of a list animates.
|
|
6888
|
+
*/
|
|
6889
|
+
function applyTransitionName(content, kind, naming) {
|
|
6890
|
+
const element = firstElementOf(content);
|
|
6891
|
+
if (element === null || namedElements.has(element)) {
|
|
6892
|
+
return;
|
|
6893
|
+
}
|
|
6894
|
+
// A node without `style` (anything outside HTMLElement / SVGElement) cannot
|
|
6895
|
+
// carry a name. Bail before touching the ledger: consuming the cap and marking
|
|
6896
|
+
// the element as named would burn a slot for a name that was never written,
|
|
6897
|
+
// and leave that element permanently ineligible.
|
|
6898
|
+
const style = element.style;
|
|
6899
|
+
if (style === undefined) {
|
|
6900
|
+
return;
|
|
6901
|
+
}
|
|
6902
|
+
const ledger = getLedger();
|
|
6903
|
+
if (ledger.assigned >= naming.limit) {
|
|
6904
|
+
if (!ledger.warned) {
|
|
6905
|
+
ledger.warned = true;
|
|
6906
|
+
console.warn(`[@wcstack/state] auto view-transition-name limit (${naming.limit}) reached; ` +
|
|
6907
|
+
"further elements are left unnamed. Raise naming-limit on <wcs-view-transition>, " +
|
|
6908
|
+
'or switch to naming="manual" and name only what should morph.');
|
|
6909
|
+
}
|
|
6910
|
+
return;
|
|
6911
|
+
}
|
|
6912
|
+
namedElements.add(element);
|
|
6913
|
+
ledger.assigned += 1;
|
|
6914
|
+
ledger.counter += 1;
|
|
6915
|
+
style.setProperty("view-transition-name", `wcs-${kind}-${ledger.counter}`);
|
|
6916
|
+
// Group handle for CSS (`::view-transition-group(*.wcs-row)`). Ignored by
|
|
6917
|
+
// engines that predate view-transition-class, which costs nothing.
|
|
6918
|
+
style.setProperty("view-transition-class", `wcs-${kind}`);
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6668
6921
|
const lastNodeByNode = new WeakMap();
|
|
6669
6922
|
const contentByListIndexByNode = new WeakMap();
|
|
6670
6923
|
const pooledContentsByNode = new WeakMap();
|
|
@@ -6832,6 +7085,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6832
7085
|
setRootNodeByFragment(fragment, context.rootNode);
|
|
6833
7086
|
}
|
|
6834
7087
|
const ssrMode = inSsr();
|
|
7088
|
+
// 自動命名ポリシーは行ごとではなく apply ごとに 1 回だけ引く
|
|
7089
|
+
// (docs/view-transition-design.md §6)。既定の manual では null で、
|
|
7090
|
+
// 以降の行ループは分岐 1 つ分しか増えない。
|
|
7091
|
+
const autoNaming = getAutoNaming();
|
|
6835
7092
|
const uuid = bindingInfo.uuid ?? '';
|
|
6836
7093
|
// 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
|
|
6837
7094
|
// 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
|
|
@@ -6875,6 +7132,9 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
6875
7132
|
}
|
|
6876
7133
|
// コンテントを活性化
|
|
6877
7134
|
activateContent(content, loopContext, context);
|
|
7135
|
+
if (autoNaming !== null) {
|
|
7136
|
+
applyTransitionName(content, "row", autoNaming);
|
|
7137
|
+
}
|
|
6878
7138
|
});
|
|
6879
7139
|
if (typeof content === 'undefined') {
|
|
6880
7140
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
@@ -6974,6 +7234,11 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
|
|
|
6974
7234
|
}
|
|
6975
7235
|
const loopContext = getLoopContextByNode(bindingInfo.node);
|
|
6976
7236
|
activateContent(content, loopContext, context);
|
|
7237
|
+
// 自動命名(docs/view-transition-design.md §6)。manual(既定)では null。
|
|
7238
|
+
const autoNaming = getAutoNaming();
|
|
7239
|
+
if (autoNaming !== null) {
|
|
7240
|
+
applyTransitionName(content, "branch", autoNaming);
|
|
7241
|
+
}
|
|
6977
7242
|
}
|
|
6978
7243
|
}
|
|
6979
7244
|
|
|
@@ -7509,12 +7774,47 @@ function missingRootPathMessage(stateName, path, target, declaredPaths) {
|
|
|
7509
7774
|
* 決まるので、噛み合わないことは常にプログラマのミス。
|
|
7510
7775
|
*/
|
|
7511
7776
|
function indexArityMessage(api, path, wildcardCount, actual) {
|
|
7777
|
+
// `$getAll` / `$setAll` の添字は前方一致の接頭辞なので上限、`$resolve` だけが厳密一致
|
|
7778
|
+
// (docs/state-set-all-design.md §4)。
|
|
7512
7779
|
const requirement = api === "$resolve"
|
|
7513
7780
|
? `exactly ${wildcardCount}`
|
|
7514
7781
|
: `at most ${wildcardCount}`;
|
|
7515
7782
|
return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
|
|
7516
7783
|
`("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
|
|
7517
7784
|
}
|
|
7785
|
+
/**
|
|
7786
|
+
* `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
|
|
7787
|
+
* 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
|
|
7788
|
+
* 文脈が添字を持っている場合、黙って全展開に倒すと「文脈で絞られている」という
|
|
7789
|
+
* 書き手の期待と食い違い、異なる文脈の添字の流用とも区別が付かないため throw する。
|
|
7790
|
+
*
|
|
7791
|
+
* 実行時の評価文脈に依存する(`$setAll` の spread 長と同種)ので lint へは誘導しない。
|
|
7792
|
+
*/
|
|
7793
|
+
function getAllContextMismatchMessage(path, contextPath) {
|
|
7794
|
+
return `$getAll("${path}") was called without indexes inside the loop context of ` +
|
|
7795
|
+
`"${contextPath}", but the path shares no wildcard level with that context, ` +
|
|
7796
|
+
`so the context indexes ($1..$n) do not apply. ` +
|
|
7797
|
+
`Pass indexes explicitly ([] expands every level).`;
|
|
7798
|
+
}
|
|
7799
|
+
/**
|
|
7800
|
+
* `$setAll(path, indexes, values, { spread: true })` の配列長がマッチ件数と噛み合わない。
|
|
7801
|
+
*
|
|
7802
|
+
* 静的には件数が分からない(実行時のリスト長に依存する)ので lint へは誘導しない。
|
|
7803
|
+
* 黙って切り詰める/余りを捨てると誤配が通ってしまうため throw する
|
|
7804
|
+
* (docs/state-set-all-design.md §3-3)。
|
|
7805
|
+
*/
|
|
7806
|
+
function setAllSpreadArityMessage(path, matched, actual) {
|
|
7807
|
+
return `$setAll("${path}", …, { spread: true }) requires the values array to have ` +
|
|
7808
|
+
`exactly one entry per matched address (matched ${matched}) but got ${actual}. ` +
|
|
7809
|
+
`Did the list change between $getAll and $setAll?`;
|
|
7810
|
+
}
|
|
7811
|
+
/**
|
|
7812
|
+
* `$setAll` の値と `options` の組み合わせが意味を成さない。
|
|
7813
|
+
* (docs/state-set-all-design.md §3-1)
|
|
7814
|
+
*/
|
|
7815
|
+
function setAllValueKindMessage(path, reason) {
|
|
7816
|
+
return `$setAll("${path}") ${reason}`;
|
|
7817
|
+
}
|
|
7518
7818
|
/**
|
|
7519
7819
|
* ワイルドカードを解決するループ文脈が足りない(=パスの階数 > スコープの階数)。
|
|
7520
7820
|
*
|
|
@@ -8453,7 +8753,7 @@ async function buildBindings(root) {
|
|
|
8453
8753
|
}
|
|
8454
8754
|
}
|
|
8455
8755
|
|
|
8456
|
-
var version = "1.
|
|
8756
|
+
var version = "1.32.0";
|
|
8457
8757
|
var pkg = {
|
|
8458
8758
|
version: version};
|
|
8459
8759
|
|
|
@@ -9176,6 +9476,221 @@ async function hydrateBindings(root) {
|
|
|
9176
9476
|
return true;
|
|
9177
9477
|
}
|
|
9178
9478
|
|
|
9479
|
+
// ===========================================================================
|
|
9480
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
9481
|
+
// Generated from /protocol/binder.ts by scripts/sync-protocol-types.mjs.
|
|
9482
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
9483
|
+
// ===========================================================================
|
|
9484
|
+
// binder protocol — how a package that inserts DOM hands those nodes to whoever
|
|
9485
|
+
// owns data bindings on the page.
|
|
9486
|
+
//
|
|
9487
|
+
// The dual of transition-runner: that one hands a *mutation* to whoever animates
|
|
9488
|
+
// it, this one hands *new nodes* to whoever binds them.
|
|
9489
|
+
//
|
|
9490
|
+
// A `data-wcs` binding exists only for nodes @wcstack/state walked when it built
|
|
9491
|
+
// its bindings. Nodes that arrive later — the content of a route that was not
|
|
9492
|
+
// active at that moment, a <wcs-head> child reflected into <head> — were never
|
|
9493
|
+
// walked, so their bindings silently do nothing, however often they are inserted.
|
|
9494
|
+
// @wcstack/router must not depend on @wcstack/state (zero runtime dependencies,
|
|
9495
|
+
// independently publishable), so state installs a binder on a well-known global
|
|
9496
|
+
// symbol and inserters look it up lazily.
|
|
9497
|
+
//
|
|
9498
|
+
// No binder installed means nothing happens — byte-for-byte the behavior these
|
|
9499
|
+
// packages had before the protocol existed.
|
|
9500
|
+
//
|
|
9501
|
+
// docs/binder-protocol-design.md is the normative description.
|
|
9502
|
+
//
|
|
9503
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/binder.ts), then run
|
|
9504
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
9505
|
+
// (packages/<pkg>/src/protocol/binder.ts). Those copies are generated — do not edit them.
|
|
9506
|
+
/**
|
|
9507
|
+
* Global key the binder installs itself under. `Symbol.for` so independently
|
|
9508
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
9509
|
+
*/
|
|
9510
|
+
const BINDER_KEY = Symbol.for("wcstack.binder");
|
|
9511
|
+
/**
|
|
9512
|
+
* The installed binder, or null when there is none or it speaks a version this
|
|
9513
|
+
* reader does not.
|
|
9514
|
+
*
|
|
9515
|
+
* Looked up on every call rather than cached, for the same reason
|
|
9516
|
+
* transition-runner does: the page's composition can change at any point, and a
|
|
9517
|
+
* stale cache would keep calling into a binder that is no longer there.
|
|
9518
|
+
*/
|
|
9519
|
+
function getBinder() {
|
|
9520
|
+
const candidate = globalThis[BINDER_KEY];
|
|
9521
|
+
if (candidate === undefined || candidate === null)
|
|
9522
|
+
return null;
|
|
9523
|
+
if (candidate.protocol !== "wcs-binder")
|
|
9524
|
+
return null;
|
|
9525
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
9526
|
+
return null;
|
|
9527
|
+
if (typeof candidate.bind !== "function")
|
|
9528
|
+
return null;
|
|
9529
|
+
return candidate;
|
|
9530
|
+
}
|
|
9531
|
+
/**
|
|
9532
|
+
* Subtrees offered before a binder existed, and the set of everything a binder
|
|
9533
|
+
* has taken. Both live on global symbols so that independently loaded copies of
|
|
9534
|
+
* this file — the router's and state's — share one queue.
|
|
9535
|
+
*
|
|
9536
|
+
* The queue is needed because of load order: the router's auto bundle runs
|
|
9537
|
+
* before state's, so `<wcs-head>` reflects its children into `<head>` while
|
|
9538
|
+
* there is still nothing to bind them. Offering them to a binder that arrives
|
|
9539
|
+
* later is the difference between working and silently blank.
|
|
9540
|
+
*/
|
|
9541
|
+
const PENDING_KEY = Symbol.for("wcstack.binder.pending");
|
|
9542
|
+
const TAKEN_KEY = Symbol.for("wcstack.binder.taken");
|
|
9543
|
+
function pendingQueue() {
|
|
9544
|
+
const globals = globalThis;
|
|
9545
|
+
let queue = globals[PENDING_KEY];
|
|
9546
|
+
if (queue === undefined) {
|
|
9547
|
+
queue = [];
|
|
9548
|
+
globals[PENDING_KEY] = queue;
|
|
9549
|
+
}
|
|
9550
|
+
return queue;
|
|
9551
|
+
}
|
|
9552
|
+
function takenSet() {
|
|
9553
|
+
const globals = globalThis;
|
|
9554
|
+
let taken = globals[TAKEN_KEY];
|
|
9555
|
+
if (taken === undefined) {
|
|
9556
|
+
taken = new WeakSet();
|
|
9557
|
+
globals[TAKEN_KEY] = taken;
|
|
9558
|
+
}
|
|
9559
|
+
return taken;
|
|
9560
|
+
}
|
|
9561
|
+
/**
|
|
9562
|
+
* Bind everything offered before this binder existed. Called by the binder right
|
|
9563
|
+
* after it installs itself.
|
|
9564
|
+
*/
|
|
9565
|
+
function flushPendingBinds() {
|
|
9566
|
+
const binder = getBinder();
|
|
9567
|
+
if (binder === null)
|
|
9568
|
+
return;
|
|
9569
|
+
const queue = pendingQueue();
|
|
9570
|
+
if (queue.length === 0)
|
|
9571
|
+
return;
|
|
9572
|
+
const pending = queue.splice(0, queue.length);
|
|
9573
|
+
const taken = takenSet();
|
|
9574
|
+
for (const subtree of pending) {
|
|
9575
|
+
taken.add(subtree);
|
|
9576
|
+
binder.bind(subtree);
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
|
|
9580
|
+
/**
|
|
9581
|
+
* binder プロトコルの提供側(docs/binder-protocol-design.md)。
|
|
9582
|
+
*
|
|
9583
|
+
* `buildBindings` は起動時に `document.body` を 1 回走査するだけなので、そのとき
|
|
9584
|
+
* document に居なかったノードのバインドは存在しない。router が後から差し込む
|
|
9585
|
+
* ルート内容や `<wcs-head>` のクローンがこれに当たり、書いたバインドが黙って
|
|
9586
|
+
* 何もしない状態になっていた。`bind()` はその取りこぼしを 1 サブツリー分だけ
|
|
9587
|
+
* 埋める。
|
|
9588
|
+
*
|
|
9589
|
+
* **走査を勝手に広げない。** MutationObserver が見た全追加ノードを走査する形に
|
|
9590
|
+
* すると、バインドを 1 個も持たない挿入(大多数)にコストが乗り、さらに
|
|
9591
|
+
* `innerHTML` で入れた外部由来の DOM が `data-wcs` を発火させることになる。
|
|
9592
|
+
* ここで束ねるのは**明示的に渡されたものだけ**である。
|
|
9593
|
+
*/
|
|
9594
|
+
const BIND_ATTRIBUTE_SELECTOR = () => `[${config.bindAttributeName}]`;
|
|
9595
|
+
/**
|
|
9596
|
+
* このサブツリーは既にバインド済みか。
|
|
9597
|
+
*
|
|
9598
|
+
* ルート内容は「起動時に active だったので全部バインド済み」か「一度も走査されて
|
|
9599
|
+
* いないので全部未バインド」のどちらかで、途中の状態を取らない。したがって
|
|
9600
|
+
* **宣言を持つ最初のノード 1 個**を見れば足りる。全ノードを走査して判定するのは
|
|
9601
|
+
* 同じ結論により高いコストを払うだけになる。
|
|
9602
|
+
*/
|
|
9603
|
+
function alreadyBound(subtree) {
|
|
9604
|
+
if (hasInterestedSession(subtree)) {
|
|
9605
|
+
return true;
|
|
9606
|
+
}
|
|
9607
|
+
if (!isElement(subtree)) {
|
|
9608
|
+
return false;
|
|
9609
|
+
}
|
|
9610
|
+
if (subtree.hasAttribute(config.bindAttributeName)) {
|
|
9611
|
+
// 属性を持つのに台帳に居ない = 未バインド
|
|
9612
|
+
return false;
|
|
9613
|
+
}
|
|
9614
|
+
const first = subtree.querySelector(BIND_ATTRIBUTE_SELECTOR());
|
|
9615
|
+
return first !== null && hasInterestedSession(first);
|
|
9616
|
+
}
|
|
9617
|
+
function isElement(node) {
|
|
9618
|
+
return node.nodeType === 1;
|
|
9619
|
+
}
|
|
9620
|
+
function bindNow(subtree) {
|
|
9621
|
+
if (alreadyBound(subtree)) {
|
|
9622
|
+
return;
|
|
9623
|
+
}
|
|
9624
|
+
convertMustacheToComments(subtree);
|
|
9625
|
+
collectStructuralFragments(subtree.getRootNode(), subtree);
|
|
9626
|
+
// `getSubscriberNodes` の TreeWalker は**ルート自身を返さない**。`buildBindings` は
|
|
9627
|
+
// `document.body` を渡すので今まで問題にならなかったが、ここには宣言をルートに
|
|
9628
|
+
// 持つノードが来る(`<wcs-head>` が head へ入れる `<title data-wcs="…">`)。
|
|
9629
|
+
// そのときだけ親から走査して、ルートを走査範囲に含める。兄弟の重複登録は
|
|
9630
|
+
// `registeredNodeSet` が弾くので、余計なバインドは生まれない。
|
|
9631
|
+
// 親は Element とは限らない(ShadowRoot 直下なら DocumentFragment、head 直下なら
|
|
9632
|
+
// Element)。`parentElement` だと前者で null になり、ルートを含められない。
|
|
9633
|
+
const declaresOnRoot = subtree.hasAttribute(config.bindAttributeName);
|
|
9634
|
+
const parent = subtree.parentNode;
|
|
9635
|
+
const canWalkFromParent = parent !== null
|
|
9636
|
+
&& (parent.nodeType === 1 || parent.nodeType === 9 || parent.nodeType === 11);
|
|
9637
|
+
const walkRoot = declaresOnRoot && canWalkFromParent
|
|
9638
|
+
? parent
|
|
9639
|
+
: subtree;
|
|
9640
|
+
initializeBindings(walkRoot, null);
|
|
9641
|
+
}
|
|
9642
|
+
/**
|
|
9643
|
+
* 初期バインド構築より前に差し出されたサブツリー。
|
|
9644
|
+
*
|
|
9645
|
+
* `<wcs-head>` は `connectedCallback` の中でクローンを head へ入れるので、
|
|
9646
|
+
* state / router のどちらを先に読み込んでも「まだ構築が終わっていない」時点で
|
|
9647
|
+
* bind を求めてくる。そこで同期に束ねても `<wcs-state>` の登録が済んでおらず、
|
|
9648
|
+
* バインドは state を見つけられない。**構築の完了を唯一の合図にする。**
|
|
9649
|
+
*/
|
|
9650
|
+
const beforeFirstBuild = [];
|
|
9651
|
+
function bind(subtree) {
|
|
9652
|
+
if (!isElement(subtree) || alreadyBound(subtree)) {
|
|
9653
|
+
return;
|
|
9654
|
+
}
|
|
9655
|
+
if (!areBindingsBuilt(subtree.getRootNode())) {
|
|
9656
|
+
beforeFirstBuild.push(subtree);
|
|
9657
|
+
return;
|
|
9658
|
+
}
|
|
9659
|
+
bindNow(subtree);
|
|
9660
|
+
}
|
|
9661
|
+
/**
|
|
9662
|
+
* 初期バインド構築の完了時に呼ぶ(stateElementByName.ts)。binder が居ない時点で
|
|
9663
|
+
* 差し出された分(プロトコルの保留キュー)と、居たが早すぎた分をまとめて束ねる。
|
|
9664
|
+
*/
|
|
9665
|
+
function drainPendingBinds() {
|
|
9666
|
+
const pending = beforeFirstBuild.splice(0, beforeFirstBuild.length);
|
|
9667
|
+
for (const subtree of pending) {
|
|
9668
|
+
bindNow(subtree);
|
|
9669
|
+
}
|
|
9670
|
+
flushPendingBinds();
|
|
9671
|
+
}
|
|
9672
|
+
const binder = {
|
|
9673
|
+
protocol: "wcs-binder",
|
|
9674
|
+
version: 1,
|
|
9675
|
+
bind,
|
|
9676
|
+
};
|
|
9677
|
+
/**
|
|
9678
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
9679
|
+
*
|
|
9680
|
+
* 既に別のコピーが載っているなら譲る。1 ページに 2 つの state バンドルが載る構成
|
|
9681
|
+
* (CDN の取り違え)で、後から読まれた側が先客を追い出すと、先客がバインドした
|
|
9682
|
+
* ノードの台帳と食い違う。
|
|
9683
|
+
*/
|
|
9684
|
+
function registerBinder() {
|
|
9685
|
+
const globals = globalThis;
|
|
9686
|
+
if (globals[BINDER_KEY] === undefined) {
|
|
9687
|
+
globals[BINDER_KEY] = binder;
|
|
9688
|
+
}
|
|
9689
|
+
// ここでは引き取らない。`<wcs-state>` の登録は connectedCallback の await より
|
|
9690
|
+
// 後なので、この時点ではまだ state が居ない。保留分は初期バインド構築の完了時に
|
|
9691
|
+
// 流す(stateElementByName.ts)。そこが「state が確実に居る」最初の瞬間である。
|
|
9692
|
+
}
|
|
9693
|
+
|
|
9179
9694
|
const stateElementByNameByNode = new WeakMap();
|
|
9180
9695
|
const bindingsReadyByNode = new WeakMap();
|
|
9181
9696
|
// devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
|
|
@@ -9198,6 +9713,25 @@ function getStateElementByName(rootNode, name) {
|
|
|
9198
9713
|
function getBindingsReady(rootNode) {
|
|
9199
9714
|
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
9200
9715
|
}
|
|
9716
|
+
const bindingsBuiltRoots = new WeakSet();
|
|
9717
|
+
/**
|
|
9718
|
+
* この rootNode の初期バインド構築が完了しているか。
|
|
9719
|
+
*
|
|
9720
|
+
* binder プロトコル(`bind()`)が使う。router の `<wcs-head>` はクローンを
|
|
9721
|
+
* `connectedCallback` の中で head へ入れるので、**state が最初の走査を終える前**に
|
|
9722
|
+
* bind を求めてくる。そこで同期に束ねても state 要素の初期化が済んでおらず、
|
|
9723
|
+
* 結果は空のままになる。完了までは binder 側で保留する。
|
|
9724
|
+
*
|
|
9725
|
+
* 「まだ登録も済んでいない」と「もう構築が終わった」を取り違えないよう、判定は
|
|
9726
|
+
* 完了の側で持つ。<wcs-state> の登録は connectedCallback の await より後に起きるので、
|
|
9727
|
+
* 「エントリの有無」で進行中かを測ると読み込み順によって逆の答えを返す。
|
|
9728
|
+
*/
|
|
9729
|
+
function areBindingsBuilt(rootNode) {
|
|
9730
|
+
return bindingsBuiltRoots.has(rootNode);
|
|
9731
|
+
}
|
|
9732
|
+
function markBindingsBuilt(rootNode) {
|
|
9733
|
+
bindingsBuiltRoots.add(rootNode);
|
|
9734
|
+
}
|
|
9201
9735
|
function setStateElementByName(rootNode, name, element) {
|
|
9202
9736
|
let stateElementByName = stateElementByNameByNode.get(rootNode);
|
|
9203
9737
|
if (element === null) {
|
|
@@ -9250,6 +9784,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
9250
9784
|
else {
|
|
9251
9785
|
await buildBindings(rootNode);
|
|
9252
9786
|
}
|
|
9787
|
+
markBindingsBuilt(rootNode);
|
|
9788
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
9789
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
9790
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
9791
|
+
drainPendingBinds();
|
|
9253
9792
|
resolve();
|
|
9254
9793
|
}
|
|
9255
9794
|
catch (error) {
|
|
@@ -9264,6 +9803,11 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
9264
9803
|
queueMicrotask(async () => {
|
|
9265
9804
|
try {
|
|
9266
9805
|
await buildBindings(rootNode);
|
|
9806
|
+
markBindingsBuilt(rootNode);
|
|
9807
|
+
// binder が居ない時点で差し出されたサブツリーを引き取る。ここが
|
|
9808
|
+
// 「state が確実に居る」最初の瞬間で、router の auto バンドルが
|
|
9809
|
+
// state のそれより先に走る順序を吸収できる唯一の場所である。
|
|
9810
|
+
drainPendingBinds();
|
|
9267
9811
|
resolve();
|
|
9268
9812
|
}
|
|
9269
9813
|
catch (error) {
|
|
@@ -9373,6 +9917,17 @@ function notifyUpdateBatchListeners(batch) {
|
|
|
9373
9917
|
registered.listener(batch);
|
|
9374
9918
|
}
|
|
9375
9919
|
}
|
|
9920
|
+
/**
|
|
9921
|
+
* 遷移越しの適用が失敗したときの報告。
|
|
9922
|
+
*
|
|
9923
|
+
* 遷移の中では例外を同期的に呼び出し元へ投げ返せない。今日の drain は
|
|
9924
|
+
* queueMicrotask の中で throw する = uncaught として観測されるので、それと同じ
|
|
9925
|
+
* 「loud に出す」挙動へ揃える。握り潰すと `$updatedCallback` の throw が黙って
|
|
9926
|
+
* 消える(README の 3 層表が定める伝播の契約が破れる)。
|
|
9927
|
+
*/
|
|
9928
|
+
function reportDeferredApplyFailure(error) {
|
|
9929
|
+
queueMicrotask(() => { throw error; });
|
|
9930
|
+
}
|
|
9376
9931
|
class Updater {
|
|
9377
9932
|
_queueUpdateRecords = [];
|
|
9378
9933
|
constructor() {
|
|
@@ -9481,12 +10036,34 @@ class Updater {
|
|
|
9481
10036
|
// 限られるが、そのとき drain フックまで道連れにすると「機構間の順序は固定」
|
|
9482
10037
|
// (README の 3 層表)が黙って破れる。例外は握らない = 伝播は維持する。
|
|
9483
10038
|
try {
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
10039
|
+
const applyBindings = () => {
|
|
10040
|
+
// context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
|
|
10041
|
+
if (propagationContextByBinding.size > 0) {
|
|
10042
|
+
applyChangeFromBindings(processBindings, propagationContextByBinding);
|
|
10043
|
+
}
|
|
10044
|
+
else {
|
|
10045
|
+
applyChangeFromBindings(processBindings);
|
|
10046
|
+
}
|
|
10047
|
+
};
|
|
10048
|
+
// View transition 参加点(docs/view-transition-design.md §7.2)。arbiter が
|
|
10049
|
+
// 居なければ runTransition はその場で applyBindings を呼び、undefined を返す
|
|
10050
|
+
// = 従来と完全に同じ同期適用。SSR では遷移そのものを持たない(G5)。
|
|
10051
|
+
//
|
|
10052
|
+
// 適用する binding が 0 本のバッチは arbiter へ渡さない。書き込みはバインドの
|
|
10053
|
+
// 有無に関わらず enqueue される(setByAddress)ため、headless なパス
|
|
10054
|
+
// (`$watch` 専用・`$streams` の内部状態・リスト置換の中間アドレス)への
|
|
10055
|
+
// 書き込みだけでもここへ到達する。それでページ全体をスナップショットするのは
|
|
10056
|
+
// 無駄なだけでなく、既定の mode="latest" では「アニメーションすべき DOM 変更が
|
|
10057
|
+
// 無い遷移」が実行中の本物の遷移をスキップしてしまう(ルート遷移が毎回途中で
|
|
10058
|
+
// 切れる/active が空撃ちで振動する)。
|
|
10059
|
+
if (inSsr() || processBindings.length === 0) {
|
|
10060
|
+
applyBindings();
|
|
9487
10061
|
}
|
|
9488
10062
|
else {
|
|
9489
|
-
|
|
10063
|
+
const pending = runTransition("state", applyBindings);
|
|
10064
|
+
if (pending !== undefined) {
|
|
10065
|
+
pending.catch(reportDeferredApplyFailure);
|
|
10066
|
+
}
|
|
9490
10067
|
}
|
|
9491
10068
|
}
|
|
9492
10069
|
finally {
|
|
@@ -9881,6 +10458,58 @@ function registerDevtoolsSource() {
|
|
|
9881
10458
|
getOrCreateHookRegistry().register(source);
|
|
9882
10459
|
}
|
|
9883
10460
|
|
|
10461
|
+
/**
|
|
10462
|
+
* ssr-snapshot プロトコルの提供側(docs/ssr-router-design.md §5)。
|
|
10463
|
+
*
|
|
10464
|
+
* `<wcs-ssr>` スナップショットを document 全体に対する最終パスとして生成する。
|
|
10465
|
+
* connectedCallback 内の inline 生成は「その時点の DOM」しか見えず、router が
|
|
10466
|
+
* 後から挿入するルート内容の構造テンプレートを取り逃がすレースがあった
|
|
10467
|
+
* (state のロード方式と文書順に依存)。renderToString が全要素の完了と
|
|
10468
|
+
* バインディング構築の後にこれを呼ぶことで、スナップショットは常に確定後の
|
|
10469
|
+
* DOM を見る。
|
|
10470
|
+
*
|
|
10471
|
+
* 複数 `enable-ssr` state の意味論は inline 生成と同一に保つ(文書順に生成・
|
|
10472
|
+
* fragment レジストリはモジュール共有・props store は生成ごとにクリア)。
|
|
10473
|
+
* その整理は本プロトコルの範囲外の既存挙動として引き継ぐ。
|
|
10474
|
+
*/
|
|
10475
|
+
function buildSsrDocument(root) {
|
|
10476
|
+
const stateTag = config.tagNames.state;
|
|
10477
|
+
const ssrTag = config.tagNames.ssr;
|
|
10478
|
+
const stateElements = root.querySelectorAll(`${stateTag}[enable-ssr]`);
|
|
10479
|
+
for (const stateEl of stateElements) {
|
|
10480
|
+
const name = stateEl.getAttribute("name") || "default";
|
|
10481
|
+
// 既に直前へ生成済み(旧 server との組み合わせで inline 生成された等)なら
|
|
10482
|
+
// 何もしない — build() は冪等でなければならない(プロトコル契約)
|
|
10483
|
+
const prev = stateEl.previousElementSibling;
|
|
10484
|
+
if (prev !== null &&
|
|
10485
|
+
prev.tagName.toLowerCase() === ssrTag &&
|
|
10486
|
+
(prev.getAttribute("name") || "default") === name) {
|
|
10487
|
+
continue;
|
|
10488
|
+
}
|
|
10489
|
+
const ssrEl = document.createElement(ssrTag);
|
|
10490
|
+
ssrEl.setAttribute("name", name);
|
|
10491
|
+
ssrEl.setAttribute("version", VERSION);
|
|
10492
|
+
Ssr.buildContent(ssrEl, Ssr.extractStateData(stateEl));
|
|
10493
|
+
stateEl.parentNode?.insertBefore(ssrEl, stateEl);
|
|
10494
|
+
}
|
|
10495
|
+
}
|
|
10496
|
+
const builder = {
|
|
10497
|
+
protocol: "wcs-ssr-snapshot",
|
|
10498
|
+
version: 1,
|
|
10499
|
+
build: buildSsrDocument,
|
|
10500
|
+
};
|
|
10501
|
+
/**
|
|
10502
|
+
* グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
|
|
10503
|
+
* binder(registerBinder)と同じ規範 — 既に別のコピーが載っているなら譲る
|
|
10504
|
+
* (そのコピーのレジストリが、そのページの正本だからである)。
|
|
10505
|
+
*/
|
|
10506
|
+
function registerSsrSnapshotBuilder() {
|
|
10507
|
+
const globals = globalThis;
|
|
10508
|
+
if (globals[SSR_SNAPSHOT_BUILDER_KEY] === undefined) {
|
|
10509
|
+
globals[SSR_SNAPSHOT_BUILDER_KEY] = builder;
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
|
|
9884
10513
|
const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
|
|
9885
10514
|
/**
|
|
9886
10515
|
* インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
|
|
@@ -9961,9 +10590,26 @@ async function loadFromJsonFile(url) {
|
|
|
9961
10590
|
}
|
|
9962
10591
|
}
|
|
9963
10592
|
|
|
10593
|
+
/**
|
|
10594
|
+
* `src` の値を **document の base URL** に対して解決する。
|
|
10595
|
+
*
|
|
10596
|
+
* `import(url)` の相対解決は「import を書いたモジュール」を基準にする。ここは
|
|
10597
|
+
* `@wcstack/state` の中なので、素の `import(url)` は `<wcs-state src>` を
|
|
10598
|
+
* **state パッケージの所在**から解決してしまう。同一オリジンに置いたページでは
|
|
10599
|
+
* たまたま一致して見えるが、CDN 一発(`https://esm.run/@wcstack/state/auto`)で
|
|
10600
|
+
* 読み込んだ瞬間に `src="/app.js"` が CDN 側の URL を指して 404 になる。
|
|
10601
|
+
*
|
|
10602
|
+
* `src` は HTML 属性なので、正しい基準は document の base URL である
|
|
10603
|
+
* (`src="*.json"` 側は `fetch` がそう解決しており、同じ属性が形式によって
|
|
10604
|
+
* 違う基準で解決されていた)。絶対 URL・`data:`・`blob:` は URL 解決で
|
|
10605
|
+
* そのまま素通りするため、既存の使い方は影響を受けない。
|
|
10606
|
+
*/
|
|
10607
|
+
function resolveAgainstDocument(url) {
|
|
10608
|
+
return new URL(url, document.baseURI).href;
|
|
10609
|
+
}
|
|
9964
10610
|
async function loadFromScriptFile(url) {
|
|
9965
10611
|
try {
|
|
9966
|
-
const module = await import(/* @vite-ignore */ url);
|
|
10612
|
+
const module = await import(/* @vite-ignore */ resolveAgainstDocument(url));
|
|
9967
10613
|
return module.default || {};
|
|
9968
10614
|
}
|
|
9969
10615
|
catch (e) {
|
|
@@ -12176,6 +12822,41 @@ function disconnectedCallback(target, _prop, receiver, _handler) {
|
|
|
12176
12822
|
}
|
|
12177
12823
|
}
|
|
12178
12824
|
|
|
12825
|
+
/**
|
|
12826
|
+
* getContextListIndex.ts
|
|
12827
|
+
*
|
|
12828
|
+
* Stateの内部APIとして、現在のプロパティ参照スコープにおける
|
|
12829
|
+
* 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
|
|
12830
|
+
* リストインデックス(IListIndex)を取得する関数です。
|
|
12831
|
+
*
|
|
12832
|
+
* 主な役割:
|
|
12833
|
+
* - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
|
|
12834
|
+
* - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
|
|
12835
|
+
*
|
|
12836
|
+
* 設計ポイント:
|
|
12837
|
+
* - 直近のプロパティ参照情報を取得
|
|
12838
|
+
* - info.indexByWildcardPathからstructuredPathのインデックスを特定
|
|
12839
|
+
* - listIndex.at(index)で該当階層のリストインデックスを取得
|
|
12840
|
+
* - パスが一致しない場合や参照が存在しない場合はnullを返す
|
|
12841
|
+
*/
|
|
12842
|
+
function getContextListIndex(handler, structuredPath) {
|
|
12843
|
+
if (handler.addressStackLength === 0) {
|
|
12844
|
+
return null;
|
|
12845
|
+
}
|
|
12846
|
+
const address = handler.lastAddressStack;
|
|
12847
|
+
if (address === null) {
|
|
12848
|
+
return null;
|
|
12849
|
+
}
|
|
12850
|
+
const index = address.pathInfo.indexByWildcardPath[structuredPath];
|
|
12851
|
+
if (typeof index === "undefined") {
|
|
12852
|
+
return null;
|
|
12853
|
+
}
|
|
12854
|
+
if (address.listIndex === null) {
|
|
12855
|
+
return null;
|
|
12856
|
+
}
|
|
12857
|
+
return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
|
|
12858
|
+
}
|
|
12859
|
+
|
|
12179
12860
|
const cacheEntryByAbsoluteStateAddress = new WeakMap();
|
|
12180
12861
|
function getCacheEntryByAbsoluteStateAddress(address) {
|
|
12181
12862
|
return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
|
|
@@ -12463,38 +13144,112 @@ function getByAddress(target, address, receiver, handler) {
|
|
|
12463
13144
|
}
|
|
12464
13145
|
|
|
12465
13146
|
/**
|
|
12466
|
-
*
|
|
13147
|
+
* wildcardIndexes.ts
|
|
12467
13148
|
*
|
|
12468
|
-
*
|
|
12469
|
-
*
|
|
12470
|
-
*
|
|
13149
|
+
* ワイルドカードを含むパスから「解決済み添字タプルの集合」を列挙する共有走査。
|
|
13150
|
+
* `$getAll`(読み)と `$setAll`(書き)が**同じ展開規則・同じ順序**で動くための単一の正本
|
|
13151
|
+
* (docs/state-set-all-design.md §6-1)。
|
|
12471
13152
|
*
|
|
12472
|
-
*
|
|
12473
|
-
*
|
|
12474
|
-
*
|
|
13153
|
+
* 添字は**前方一致の接頭辞**で、足りない分は「その階層を全部展開する」という意味を持つ
|
|
13154
|
+
* (README の `$getAll("scores.*", [])` がこれ)。返るタプルは常にワイルドカードの本数と
|
|
13155
|
+
* 同じ長さになるので、そのまま `$resolve` の厳密一致な添字として使える。
|
|
12475
13156
|
*
|
|
12476
|
-
*
|
|
12477
|
-
*
|
|
12478
|
-
*
|
|
12479
|
-
*
|
|
12480
|
-
* -
|
|
13157
|
+
* 順序は**深さ優先・添字昇順**(ネストは添字タプルの辞書順)で決定的。
|
|
13158
|
+
* `$getAll(p, i)` の戻り順と `$setAll(p, i, …)` の適用順が一致する根拠がこれであり、
|
|
13159
|
+
* `$setAll` の `{ spread: true }` 形はこの順序に乗っている。
|
|
13160
|
+
*
|
|
13161
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
|
|
12481
13162
|
*/
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
13163
|
+
/**
|
|
13164
|
+
* 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
|
|
13165
|
+
* ListIndex の同一性を跨いで保つために使う。
|
|
13166
|
+
*
|
|
13167
|
+
* 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
|
|
13168
|
+
* 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
|
|
13169
|
+
*/
|
|
13170
|
+
// ToDo: IAbsoluteStateAddressに変更する
|
|
13171
|
+
const lastValueByListAddress = new WeakMap();
|
|
13172
|
+
/**
|
|
13173
|
+
* `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
|
|
13174
|
+
* マッチする添字タプルを列挙する。
|
|
13175
|
+
*
|
|
13176
|
+
* 添字の本数検査(上限)は呼び出し側の責務 — API 名を診断メッセージに出すため。
|
|
13177
|
+
*/
|
|
13178
|
+
function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, options) {
|
|
13179
|
+
const newValueByAddress = new Map();
|
|
13180
|
+
const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
|
|
13181
|
+
const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
|
|
13182
|
+
if (wildcardParentPathInfo === null) {
|
|
13183
|
+
results.push(parentIndexes);
|
|
13184
|
+
return;
|
|
13185
|
+
}
|
|
13186
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13187
|
+
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13188
|
+
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13189
|
+
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13190
|
+
const listIndexes = listDiff.newIndexes;
|
|
13191
|
+
const index = indexes[indexPos] ?? null;
|
|
13192
|
+
newValueByAddress.set(wildcardAddress, newValue);
|
|
13193
|
+
if (index === null) {
|
|
13194
|
+
for (let i = 0; i < listIndexes.length; i++) {
|
|
13195
|
+
const listIndex = listIndexes[i];
|
|
13196
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
else {
|
|
13200
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13201
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13202
|
+
const listIndex = listIndexes[index] ??
|
|
13203
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13204
|
+
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13205
|
+
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13206
|
+
}
|
|
13207
|
+
else {
|
|
13208
|
+
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13209
|
+
results.push(parentIndexes.concat(listIndex.index));
|
|
13210
|
+
}
|
|
13211
|
+
}
|
|
13212
|
+
};
|
|
13213
|
+
const resultIndexes = [];
|
|
13214
|
+
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
13215
|
+
if (options.commitDiffBaseline) {
|
|
13216
|
+
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13217
|
+
lastValueByListAddress.set(address, newValue);
|
|
13218
|
+
}
|
|
12496
13219
|
}
|
|
12497
|
-
return
|
|
13220
|
+
return resultIndexes;
|
|
13221
|
+
}
|
|
13222
|
+
|
|
13223
|
+
/**
|
|
13224
|
+
* getListIndexByIndexes.ts
|
|
13225
|
+
*
|
|
13226
|
+
* 解決済みの添字タプル(ワイルドカード 1 段につき 1 個)から、対応する ListIndex を
|
|
13227
|
+
* **正本レジストリ**(listIndexesByList)経由で引き当てる。
|
|
13228
|
+
*
|
|
13229
|
+
* `$resolve` と `$setAll` の共有部分。列挙側(wildcardIndexes.ts)が走査中に生成した
|
|
13230
|
+
* ListIndex をそのまま書き込み先にせず、ここで引き直すことで、binding が使っている
|
|
13231
|
+
* ListIndex と同一の同一性に載る(docs/state-set-all-design.md §6-2)。
|
|
13232
|
+
*
|
|
13233
|
+
* 添字の本数がワイルドカードの本数と一致していることは呼び出し側の責務。
|
|
13234
|
+
*/
|
|
13235
|
+
function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
|
|
13236
|
+
// ワイルドカード階層ごとにListIndexを解決していく
|
|
13237
|
+
let listIndex = null;
|
|
13238
|
+
for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
|
|
13239
|
+
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13240
|
+
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13241
|
+
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13242
|
+
const listIndexes = getListIndexesByList(tmpValue);
|
|
13243
|
+
if (listIndexes == null) {
|
|
13244
|
+
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13245
|
+
}
|
|
13246
|
+
const index = indexes[i];
|
|
13247
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13248
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13249
|
+
listIndex = listIndexes[index] ??
|
|
13250
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13251
|
+
}
|
|
13252
|
+
return listIndex;
|
|
12498
13253
|
}
|
|
12499
13254
|
|
|
12500
13255
|
/**
|
|
@@ -13500,22 +14255,8 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13500
14255
|
if (indexes.length !== pathInfo.wildcardParentPathInfos.length) {
|
|
13501
14256
|
raiseError(indexArityMessage("$resolve", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
13502
14257
|
}
|
|
13503
|
-
// ワイルドカード階層ごとにListIndex
|
|
13504
|
-
|
|
13505
|
-
for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
|
|
13506
|
-
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
13507
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13508
|
-
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13509
|
-
const listIndexes = getListIndexesByList(tmpValue);
|
|
13510
|
-
if (listIndexes == null) {
|
|
13511
|
-
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
13512
|
-
}
|
|
13513
|
-
const index = indexes[i];
|
|
13514
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13515
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13516
|
-
listIndex = listIndexes[index] ??
|
|
13517
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13518
|
-
}
|
|
14258
|
+
// ワイルドカード階層ごとにListIndexを解決していく(`$setAll` と共有)
|
|
14259
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, indexes);
|
|
13519
14260
|
// ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
|
|
13520
14261
|
const address = createStateAddress(pathInfo, listIndex);
|
|
13521
14262
|
const hasSetValue = typeof value !== "undefined";
|
|
@@ -13532,14 +14273,18 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
13532
14273
|
* getAllReadonly
|
|
13533
14274
|
*
|
|
13534
14275
|
* ワイルドカードを含む State パスから、対象となる全要素を配列で取得する。
|
|
13535
|
-
*
|
|
14276
|
+
* 走査そのものは `$setAll` と共有する(wildcardIndexes.ts)。
|
|
14277
|
+
*
|
|
14278
|
+
* `indexes` 省略時の既定はループ文脈の添字 `[$1..$n]`。正確には「path と文脈が
|
|
14279
|
+
* 共有するワイルドカード連鎖の分だけ文脈の添字を接頭辞として敷く」(整合最長接頭辞)。
|
|
14280
|
+
* 共有が無いのに文脈が添字を持つ場合は throw する — 異なる文脈の添字は流用しない。
|
|
14281
|
+
*
|
|
14282
|
+
* Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)、
|
|
14283
|
+
* 添字本数超過(wcs/index-arity)、省略時の文脈不整合(getAllContextMismatchMessage)
|
|
13536
14284
|
*/
|
|
13537
|
-
// ToDo: IAbsoluteStateAddressに変更する
|
|
13538
|
-
const lastValueByListAddress = new WeakMap();
|
|
13539
14285
|
function getAll(target, prop, receiver, handler) {
|
|
13540
14286
|
const resolveFn = resolve(target, prop, receiver, handler);
|
|
13541
14287
|
return (path, indexes) => {
|
|
13542
|
-
const newValueByAddress = new Map();
|
|
13543
14288
|
const pathInfo = getPathInfo(path);
|
|
13544
14289
|
if (handler.addressStackLength > 0) {
|
|
13545
14290
|
const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
|
|
@@ -13560,60 +14305,39 @@ function getAll(target, prop, receiver, handler) {
|
|
|
13560
14305
|
raiseError(indexArityMessage("$getAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
13561
14306
|
}
|
|
13562
14307
|
if (typeof indexes === "undefined") {
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
14308
|
+
// 省略時の既定はループ文脈の添字 `[$1..$n]`。ただし敷けるのは path と文脈が
|
|
14309
|
+
// **共有するワイルドカード連鎖**の分だけなので、path のワイルドカードを
|
|
14310
|
+
// 内側(最深)から探し、最初に文脈にヒットした階層の scoped indexes を接頭辞にする。
|
|
14311
|
+
// ワイルドカードパスの序数はパス文字列自身の `*` の本数で決まるため、深い側が
|
|
14312
|
+
// ヒットすれば浅い側は必ず含まれ、これが整合する最長の接頭辞になる。文脈が
|
|
14313
|
+
// path より深い分は自然に切り詰められ、導出した接頭辞は path のワイルドカード
|
|
14314
|
+
// 本数を超えないので、上の本数検査には掛けない。
|
|
14315
|
+
for (let i = pathInfo.wildcardPaths.length - 1; i >= 0; i--) {
|
|
14316
|
+
const listIndex = getContextListIndex(handler, pathInfo.wildcardPaths[i]);
|
|
13566
14317
|
if (listIndex) {
|
|
13567
14318
|
indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
|
|
13568
14319
|
break;
|
|
13569
14320
|
}
|
|
13570
14321
|
}
|
|
13571
14322
|
if (typeof indexes === "undefined") {
|
|
14323
|
+
// 共有ゼロ。文脈が自スコープの添字を実際に持っているなら、既定の `[...$n]` は
|
|
14324
|
+
// **異なる文脈の添字の流用(混入)**になるため、黙って全展開へ倒さず throw する。
|
|
14325
|
+
// 文脈そのものが無い(トップレベル getter / メソッド直下)なら全展開が既定。
|
|
14326
|
+
const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
|
|
14327
|
+
const contextListIndex = lastAddress?.listIndex ?? null;
|
|
14328
|
+
if (pathInfo.wildcardCount > 0 && lastAddress !== null && contextListIndex !== null &&
|
|
14329
|
+
contextListIndex.length - getBaseDepth(handler.stateElement) > 0) {
|
|
14330
|
+
raiseError(getAllContextMismatchMessage(path, lastAddress.pathInfo.path));
|
|
14331
|
+
}
|
|
13572
14332
|
indexes = [];
|
|
13573
14333
|
}
|
|
13574
14334
|
}
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
if (wildcardParentPathInfo === null) {
|
|
13578
|
-
results.push(parentIndexes);
|
|
13579
|
-
return;
|
|
13580
|
-
}
|
|
13581
|
-
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
13582
|
-
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
13583
|
-
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
13584
|
-
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
13585
|
-
const listIndexes = listDiff.newIndexes;
|
|
13586
|
-
const index = indexes[indexPos] ?? null;
|
|
13587
|
-
newValueByAddress.set(wildcardAddress, newValue);
|
|
13588
|
-
if (index === null) {
|
|
13589
|
-
for (let i = 0; i < listIndexes.length; i++) {
|
|
13590
|
-
const listIndex = listIndexes[i];
|
|
13591
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13592
|
-
}
|
|
13593
|
-
}
|
|
13594
|
-
else {
|
|
13595
|
-
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
13596
|
-
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
13597
|
-
const listIndex = listIndexes[index] ??
|
|
13598
|
-
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
13599
|
-
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
13600
|
-
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
13601
|
-
}
|
|
13602
|
-
else {
|
|
13603
|
-
// 最終ワイルドカード層まで到達しているので、結果を確定
|
|
13604
|
-
results.push(parentIndexes.concat(listIndex.index));
|
|
13605
|
-
}
|
|
13606
|
-
}
|
|
13607
|
-
};
|
|
13608
|
-
const resultIndexes = [];
|
|
13609
|
-
walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
|
|
14335
|
+
// 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
|
|
14336
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
|
|
13610
14337
|
const resultValues = [];
|
|
13611
14338
|
for (let i = 0; i < resultIndexes.length; i++) {
|
|
13612
14339
|
resultValues.push(resolveFn(pathInfo.path, resultIndexes[i]));
|
|
13613
14340
|
}
|
|
13614
|
-
for (const [address, newValue] of newValueByAddress.entries()) {
|
|
13615
|
-
lastValueByListAddress.set(address, newValue);
|
|
13616
|
-
}
|
|
13617
14341
|
return resultValues;
|
|
13618
14342
|
};
|
|
13619
14343
|
}
|
|
@@ -13697,6 +14421,83 @@ function postUpdate(target, _prop, receiver, handler) {
|
|
|
13697
14421
|
};
|
|
13698
14422
|
}
|
|
13699
14423
|
|
|
14424
|
+
/**
|
|
14425
|
+
* setAll.ts
|
|
14426
|
+
*
|
|
14427
|
+
* ワイルドカードを含む State パスにマッチする**全アドレスへ一括で書き込む**。
|
|
14428
|
+
* `$getAll`(読み)の対称形(docs/state-set-all-design.md)。
|
|
14429
|
+
*
|
|
14430
|
+
* 存在理由は糖衣ではなく「**リスト全置換の回避**」(設計 §1-1)。
|
|
14431
|
+
* `this.users = this.users.map(...)` は配列を作り直すので ListIndex・行 getter
|
|
14432
|
+
* キャッシュ・差分描画がまとめて作り直しになる。`$setAll` は意味としては一括更新、
|
|
14433
|
+
* 実体は in-place な個別書き込みで、同じことを差分に載せたまま行う。
|
|
14434
|
+
*
|
|
14435
|
+
* 3 つの形(設計 §2):
|
|
14436
|
+
* - ブロードキャスト `$setAll(path, indexes, value)`
|
|
14437
|
+
* - mapper(第一級) `$setAll(path, indexes, (current, ...indexes) => next)`
|
|
14438
|
+
* - spread `$setAll(path, indexes, values, { spread: true })`
|
|
14439
|
+
*/
|
|
14440
|
+
function setAll(target, _prop, receiver, handler) {
|
|
14441
|
+
return (path, indexes, value, options) => {
|
|
14442
|
+
const pathInfo = getPathInfo(path);
|
|
14443
|
+
// 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
|
|
14444
|
+
// 「現在行」ではなく「全行」を意味する(設計 §4-1)。
|
|
14445
|
+
if (!Array.isArray(indexes)) {
|
|
14446
|
+
raiseError(setAllValueKindMessage(path, "requires an explicit indexes array (pass [] to expand every level)."));
|
|
14447
|
+
}
|
|
14448
|
+
// 添字は前方一致の接頭辞なので不足は正当。超過だけを弾く(`$getAll` と同じ規則)。
|
|
14449
|
+
if (indexes.length > pathInfo.wildcardParentPathInfos.length) {
|
|
14450
|
+
raiseError(indexArityMessage("$setAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
|
|
14451
|
+
}
|
|
14452
|
+
const spread = options?.spread === true;
|
|
14453
|
+
const isMapper = typeof value === "function";
|
|
14454
|
+
if (spread && isMapper) {
|
|
14455
|
+
raiseError(setAllValueKindMessage(path, "cannot combine { spread: true } with a mapper function."));
|
|
14456
|
+
}
|
|
14457
|
+
if (spread && !Array.isArray(value)) {
|
|
14458
|
+
raiseError(setAllValueKindMessage(path, "requires an array as the value when { spread: true } is set."));
|
|
14459
|
+
}
|
|
14460
|
+
// --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
|
|
14461
|
+
// 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
|
|
14462
|
+
// 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
|
|
14463
|
+
const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
|
|
14464
|
+
if (spread && value.length !== resultIndexes.length) {
|
|
14465
|
+
raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
|
|
14466
|
+
}
|
|
14467
|
+
const addresses = [];
|
|
14468
|
+
for (let i = 0; i < resultIndexes.length; i++) {
|
|
14469
|
+
const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, resultIndexes[i]);
|
|
14470
|
+
addresses.push(createStateAddress(pathInfo, listIndex));
|
|
14471
|
+
}
|
|
14472
|
+
// --- 第 2 相: 確定したアドレスにだけ書く ---
|
|
14473
|
+
let written = 0;
|
|
14474
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
14475
|
+
const address = addresses[i];
|
|
14476
|
+
let nextValue;
|
|
14477
|
+
if (isMapper) {
|
|
14478
|
+
// 現在値は書く直前に読む。先行する書き込みが getter 経由で他行に及ぶ場合、
|
|
14479
|
+
// mapper が見るべきなのは最新値。
|
|
14480
|
+
const currentValue = getByAddress(target, address, receiver, handler);
|
|
14481
|
+
nextValue = value(currentValue, ...resultIndexes[i]);
|
|
14482
|
+
}
|
|
14483
|
+
else if (spread) {
|
|
14484
|
+
nextValue = value[i];
|
|
14485
|
+
}
|
|
14486
|
+
else {
|
|
14487
|
+
nextValue = value;
|
|
14488
|
+
}
|
|
14489
|
+
// undefined は常にスキップ(設計 §5)。mapper の return 忘れで全行を潰さないため、
|
|
14490
|
+
// かつ「この行は変えない」を表現できるようにするため。クリアは null。
|
|
14491
|
+
if (typeof nextValue === "undefined") {
|
|
14492
|
+
continue;
|
|
14493
|
+
}
|
|
14494
|
+
setByAddress(target, address, nextValue, receiver, handler);
|
|
14495
|
+
written++;
|
|
14496
|
+
}
|
|
14497
|
+
return written;
|
|
14498
|
+
};
|
|
14499
|
+
}
|
|
14500
|
+
|
|
13700
14501
|
/**
|
|
13701
14502
|
* trackDependency.ts
|
|
13702
14503
|
*
|
|
@@ -13871,7 +14672,7 @@ function setLoopContext(handler, loopContext, callback) {
|
|
|
13871
14672
|
* StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
|
|
13872
14673
|
*
|
|
13873
14674
|
* 主な役割:
|
|
13874
|
-
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
|
|
14675
|
+
* - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $setAll, $postUpdate,
|
|
13875
14676
|
* $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
|
|
13876
14677
|
* - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
|
|
13877
14678
|
* - getByRefで構造化パス・リストインデックスに対応した値を取得
|
|
@@ -13934,6 +14735,11 @@ function get(target, prop, receiver, handler) {
|
|
|
13934
14735
|
return getAll(target, prop, receiver, handler)(path, indexes);
|
|
13935
14736
|
};
|
|
13936
14737
|
}
|
|
14738
|
+
case "$setAll": {
|
|
14739
|
+
return (path, indexes, value, options) => {
|
|
14740
|
+
return setAll(target, prop, receiver, handler)(path, indexes, value, options);
|
|
14741
|
+
};
|
|
14742
|
+
}
|
|
13937
14743
|
case "$postUpdate": {
|
|
13938
14744
|
return (path) => {
|
|
13939
14745
|
return postUpdate(target, prop, receiver, handler)(path);
|
|
@@ -14896,8 +15702,12 @@ class State extends HTMLElementBase {
|
|
|
14896
15702
|
if (!this.hasAttribute('enable-ssr') || inSsr()) {
|
|
14897
15703
|
await this._callStateConnectedCallback();
|
|
14898
15704
|
}
|
|
14899
|
-
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr>
|
|
14900
|
-
|
|
15705
|
+
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
|
|
15706
|
+
// orchestrated(サーバー主導の最終パス、docs/ssr-router-design.md §5)では
|
|
15707
|
+
// 生成しない — renderToString が全要素の完了後にまとめて生成するため。
|
|
15708
|
+
// ここで生成すると、router 等が後から挿入した内容の構造テンプレートを
|
|
15709
|
+
// 取り逃がすレースがある(state のロード方式と文書順に依存)
|
|
15710
|
+
if (inSsr() && this.hasAttribute('enable-ssr') && !isOrchestratedSsr()) {
|
|
14901
15711
|
try {
|
|
14902
15712
|
await getBindingsReady(this.rootNode);
|
|
14903
15713
|
const name = this.getAttribute('name') || 'default';
|
|
@@ -15199,11 +16009,63 @@ function registerComponents(registry = customElements) {
|
|
|
15199
16009
|
}
|
|
15200
16010
|
}
|
|
15201
16011
|
|
|
16012
|
+
/**
|
|
16013
|
+
* `<html lang>` を既定ロケールとして採る。
|
|
16014
|
+
*
|
|
16015
|
+
* ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は `config.locale`
|
|
16016
|
+
* を読むが、それを設定できる公開の入口は `bootstrapState({ locale })` しかない。
|
|
16017
|
+
* 一方 `auto` エントリは `bootstrapState()` を引数なしで呼ぶため、CDN 一発
|
|
16018
|
+
* (`<script src=".../@wcstack/state/auto">`)で読み込んだページには**ロケールを
|
|
16019
|
+
* 渡す口が無かった**。auto バンドルは SRI のため自己完結で、別途 `@wcstack/state`
|
|
16020
|
+
* を import して `bootstrapState` を呼んでも別インスタンスになり効かない。
|
|
16021
|
+
*
|
|
16022
|
+
* `<html lang>` はページのロケールを書く HTML 標準の場所であり、SSR ではサーバーが、
|
|
16023
|
+
* 静的ページでは head のスニペットが DOM 解析前に書く。そこを既定にすると
|
|
16024
|
+
* **ロケールの正本が 1 つになり**、「設定を早く呼ぶ」という守りにくい順序の約束が
|
|
16025
|
+
* 「`<html lang>` が state のロードより前にある」という構造的な保証に変わる。
|
|
16026
|
+
*
|
|
16027
|
+
* 明示指定(`bootstrapState({ locale })`)が常に優先する。
|
|
16028
|
+
*/
|
|
16029
|
+
function localeFromDocument() {
|
|
16030
|
+
const lang = document.documentElement?.lang;
|
|
16031
|
+
if (!lang) {
|
|
16032
|
+
return undefined;
|
|
16033
|
+
}
|
|
16034
|
+
try {
|
|
16035
|
+
// 妥当な BCP-47 タグでなければ Intl が RangeError を投げる。不正な lang を
|
|
16036
|
+
// そのまま採ると、これまで既定 'en' で動いていたページのフィルタが実行時に
|
|
16037
|
+
// 落ちる。既定へ落として警告するほうが、黙って壊すより回復しやすい。
|
|
16038
|
+
Intl.getCanonicalLocales(lang);
|
|
16039
|
+
return lang;
|
|
16040
|
+
}
|
|
16041
|
+
catch {
|
|
16042
|
+
console.warn(`[@wcstack/state] <html lang="${lang}"> is not a valid BCP-47 language tag. ` +
|
|
16043
|
+
`Falling back to the default locale for filters.`);
|
|
16044
|
+
return undefined;
|
|
16045
|
+
}
|
|
16046
|
+
}
|
|
16047
|
+
function resolveConfig(config) {
|
|
16048
|
+
if (typeof config?.locale === "string") {
|
|
16049
|
+
return config;
|
|
16050
|
+
}
|
|
16051
|
+
const locale = localeFromDocument();
|
|
16052
|
+
if (locale === undefined) {
|
|
16053
|
+
return config;
|
|
16054
|
+
}
|
|
16055
|
+
return { ...config, locale };
|
|
16056
|
+
}
|
|
15202
16057
|
function bootstrapState(config, registry) {
|
|
15203
|
-
|
|
15204
|
-
|
|
16058
|
+
const resolved = resolveConfig(config);
|
|
16059
|
+
if (resolved) {
|
|
16060
|
+
setConfig(resolved);
|
|
15205
16061
|
}
|
|
15206
16062
|
registerComponents(registry);
|
|
16063
|
+
// binder プロトコルの提供(docs/binder-protocol-design.md)。router が後から
|
|
16064
|
+
// 差し込むノードをバインドできるようにする。登録は冪等。
|
|
16065
|
+
registerBinder();
|
|
16066
|
+
// ssr-snapshot プロトコルの提供(docs/ssr-router-design.md §5)。renderToString が
|
|
16067
|
+
// <wcs-ssr> 生成をサーバー主導の最終パスへ回せるようにする。登録は冪等。
|
|
16068
|
+
registerSsrSnapshotBuilder();
|
|
15207
16069
|
// DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
|
|
15208
16070
|
registerDevtoolsSource();
|
|
15209
16071
|
}
|