@wcstack/router 2.1.1 → 2.3.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/dist/index.d.ts CHANGED
@@ -78,14 +78,73 @@ declare function bootstrapRouter(config?: Partial<IWritableConfig>, registry?: C
78
78
 
79
79
  declare function getConfig(): IConfig;
80
80
 
81
+ /**
82
+ * Trusted Types (`require-trusted-types-for 'script'`) 対応。正本は docs/csp.md §7。
83
+ *
84
+ * router が HTML sink に流すのは `<wcs-layout>` のテンプレート、つまり **作者が書いた
85
+ * マークアップ**(同一文書の `<template>` か `src` で取りに行くアプリの資産)であって、
86
+ * ユーザー入力ではない。Lit がテンプレートリテラルにだけ policy を当てているのと同じ
87
+ * 立て付けで、ここは identity policy で署名してよい層に当たる。
88
+ *
89
+ * policy 名は全 @wcstack パッケージで単一の `wcstack` に固定する。利用側の CSP に
90
+ * 書く行を 1 本に固定できるほうが、パッケージごとに名前を分けて最小権限にするより
91
+ * 導入摩擦が小さいため(署名対象がどれも作者制御の値なので、分割しても守るものが
92
+ * 増えない)。生成結果はグローバルスロットで共有し、複数パッケージが同居しても
93
+ * `createPolicy` は 1 回だけ走らせる — `trusted-types` ディレクティブがある場合、
94
+ * `'allow-duplicates'` 無しの重複生成は例外になるため。
95
+ *
96
+ * **利用側が注入した policy はここでは優先しない。** 注入口は「信頼できない値に対する
97
+ * sanitizer」として使われる想定で(fetch のレスポンス、state の値)、実際に案内している
98
+ * 設定も DOMPurify である。作者が書いたレイアウトを sanitizer に通すと、既定でカスタム
99
+ * 要素が除去されて `<wcs-link>` などがレイアウトから消える——例外も警告も無しに。
100
+ * よってここは identity policy を優先し、**それを作れなかったときだけ**利用側 policy に
101
+ * 落ちる(その場合は 1 度警告する)。
102
+ */
103
+ interface IWcsTrustedTypesPolicy {
104
+ createHTML?(input: string): unknown;
105
+ createScriptURL?(input: string): unknown;
106
+ }
107
+ /** 利用側が policy を差し込むグローバルスロット(全 @wcstack パッケージ共通)。 */
108
+ declare const TRUSTED_TYPES_POLICY_SLOT: unique symbol;
109
+ /** 利用側が注入した policy。 */
110
+ declare function getTrustedTypesPolicy(): IWcsTrustedTypesPolicy | null;
111
+ /** 利用側 policy を設定する(`null` で解除)。 */
112
+ declare function setTrustedTypesPolicy(policy: IWcsTrustedTypesPolicy | null): void;
113
+
81
114
  interface IRouteMatchResult {
82
115
  routes: IRoute[];
83
116
  params: Record<string, string>;
84
117
  typedParams: Record<string, any>;
85
118
  path: string;
86
119
  lastPath: string;
120
+ /** 現在 URL のクエリ("?k=v" 形式または "")。applyRoute が commit 用に供給する */
121
+ search?: string;
122
+ /** guard 相で guard 関数が返したロード済みデータ(runGuardPhase が書く)。無ければ null */
123
+ data?: GuardData | null;
124
+ }
125
+ /**
126
+ * guard 判定関数の第 3 引数 — 進入先マッチのスナップショット。`<wcs-router>` が
127
+ * commit 後に露出する観測面と同じ語彙(params / typedParams / searchParams / routeName)を
128
+ * commit **前**に読める。frozen。
129
+ */
130
+ interface IGuardContext {
131
+ readonly params: Record<string, string>;
132
+ readonly typedParams: Record<string, any>;
133
+ readonly searchParams: Record<string, string>;
134
+ readonly routeName: string;
87
135
  }
88
- type GuardHandler = (toPath: string, fromPath: string) => boolean | Promise<boolean>;
136
+ /** guard 関数がオブジェクトを返したときの「ロード済みデータ」。`<wcs-router>.data` に載る */
137
+ type GuardData = Record<string, unknown>;
138
+ /**
139
+ * guard 判定関数の返り値:
140
+ * - `true` — 進入を許可
141
+ * - オブジェクト — 進入を許可し、そのオブジェクトを `<wcs-router>.data` として commit する
142
+ * (ルートチェーンの複数 guard が返した場合は親→子の順で浅くマージ)
143
+ * - 空でない文字列 — 進入を拒否し、その絶対パスへリダイレクト(動的リダイレクト先)
144
+ * - `false` / それ以外の falsy — 進入を拒否し、`guard` 属性のパスへリダイレクト
145
+ */
146
+ type GuardResult = boolean | string | GuardData | null | undefined;
147
+ type GuardHandler = (toPath: string, fromPath: string, context: IGuardContext) => GuardResult | Promise<GuardResult>;
89
148
  interface _ILayout {
90
149
  readonly uuid: string;
91
150
  readonly enableShadowRoot: boolean;
@@ -132,7 +191,8 @@ interface IRoute extends IRouteChildContainer {
132
191
  readonly hasGuard: boolean;
133
192
  guardHandler: GuardHandler;
134
193
  shouldChange(newParams: Record<string, string>): boolean;
135
- guardCheck(matchResult: IRouteMatchResult): Promise<void>;
194
+ /** guard 相。拒否は GuardCancel を throw。許可時は guard 関数が返したデータ(無ければ null) */
195
+ guardCheck(matchResult: IRouteMatchResult): Promise<GuardData | null>;
136
196
  initialize(routerNode: IRouter, parentRouteNode: IRoute | null): void;
137
197
  testAncestorNode(ancestorNode: IRoute): boolean;
138
198
  setParams(params: Record<string, string>, typedParams: Record<string, any>): void;
@@ -155,6 +215,8 @@ interface IRouterCommit {
155
215
  search: string;
156
216
  /** basename スライス後の path */
157
217
  path: string;
218
+ /** guard 相で集めたロード済みデータ。省略・null は「このナビゲーションにデータ無し」= null に戻す */
219
+ data?: GuardData | null;
158
220
  }
159
221
  interface IRouter extends IRouteChildContainer {
160
222
  readonly basename: string;
@@ -170,6 +232,11 @@ interface IRouter extends IRouteChildContainer {
170
232
  readonly searchParams: Record<string, string>;
171
233
  /** 最深マッチルートの name 属性値。fallback 時は fallback ルートの name */
172
234
  readonly routeName: string;
235
+ /**
236
+ * 現在マッチの guard 関数が返したロード済みデータ。guard がオブジェクトを返さなかった
237
+ * ナビゲーションでは null。same-match(クエリのみの遷移)では前の値を保つ
238
+ */
239
+ readonly data: GuardData | null;
173
240
  navigate(path: string): Promise<void>;
174
241
  /** navigateUrl(push)の対になる replace 遷移(§4.2) */
175
242
  replace(path: string): Promise<void>;
@@ -241,6 +308,7 @@ declare class Router extends HTMLElement implements IRouter {
241
308
  private _typedParams;
242
309
  private _searchParams;
243
310
  private _routeName;
311
+ private _data;
244
312
  /** 最初の成功 commit を通過したか(§4.4 の初回ガード) */
245
313
  private _hasCommitted;
246
314
  private _connectedCallbackPromise;
@@ -296,6 +364,7 @@ declare class Router extends HTMLElement implements IRouter {
296
364
  get typedParams(): Record<string, any>;
297
365
  get searchParams(): Record<string, string>;
298
366
  get routeName(): string;
367
+ get data(): GuardData | null;
299
368
  /**
300
369
  * same-match 判定(docs/router-state-contract-design.md §4.4)。
301
370
  *
@@ -313,9 +382,10 @@ declare class Router extends HTMLElement implements IRouter {
313
382
  *
314
383
  * 全内部値を先にコミットし、その後で初めてイベントを発火する — どのイベントの
315
384
  * リスナーから要素プロパティを読んでも、遷移後スナップショットの一貫した値が
316
- * 見える。発火順序は params → route-name → search → path。`path` を最後に
317
- * 置くのは、既存例で `path` が「ナビゲーション完了」の信号として使われている
318
- * ため。各イベントは変化した commit のみ発火する。
385
+ * 見える。発火順序は data → params → route-name → search → path。`data`
386
+ * 先頭に置くのは、params のリスナーがロード済みデータを読めるようにするため。
387
+ * `path` を最後に置くのは、既存例で `path` が「ナビゲーション完了」の信号として
388
+ * 使われているため。各イベントは変化した commit のみ発火する。
319
389
  */
320
390
  commitNavigation(commit: IRouterCommit): void;
321
391
  get fallbackRoute(): IRoute | null;
@@ -428,7 +498,7 @@ declare class Route extends HTMLElement implements IRoute {
428
498
  setParams(params: Record<string, string>, typedParams: Record<string, any>): void;
429
499
  clearParams(): void;
430
500
  shouldChange(newParams: Record<string, string>): boolean;
431
- guardCheck(matchResult: IRouteMatchResult): Promise<void>;
501
+ guardCheck(matchResult: IRouteMatchResult): Promise<GuardData | null>;
432
502
  notifyGuardHandlerLoadFailed(): void;
433
503
  /**
434
504
  * Shell(Route)の routeParentNode を辿って祖先関係を判定する。
@@ -522,7 +592,14 @@ declare class RouteCore extends EventTarget {
522
592
  * 解除後の guardCheck は guardHandler が未設定のため fallback パスへリダイレクトする。
523
593
  */
524
594
  notifyGuardHandlerLoadFailed(): void;
525
- guardCheck(matchResult: IRouteMatchResult): Promise<void>;
595
+ /**
596
+ * guard 相(components/types.ts の GuardResult を参照)。
597
+ * - 空でない文字列 → その絶対パスへ動的リダイレクト(`guard` 属性より優先)
598
+ * - falsy(false / undefined / null / "")→ `guard` 属性のパスへ
599
+ * - オブジェクト → 許可し、ロード済みデータとして返す(runGuardPhase が集約)
600
+ * - true → 許可(データ無し = null)
601
+ */
602
+ guardCheck(matchResult: IRouteMatchResult): Promise<GuardData | null>;
526
603
  }
527
604
 
528
605
  declare const VERSION: string;
@@ -685,5 +762,5 @@ declare global {
685
762
  }
686
763
  }
687
764
 
688
- export { Route, RouteCore, Router, VERSION, bootstrapRouter, getConfig };
689
- export type { IWritableConfig, IWritableTagNames, RouteParseOptions };
765
+ export { Route, RouteCore, Router, TRUSTED_TYPES_POLICY_SLOT, VERSION, bootstrapRouter, getConfig, getTrustedTypesPolicy, setTrustedTypesPolicy };
766
+ export type { IWcsTrustedTypesPolicy, IWritableConfig, IWritableTagNames, RouteParseOptions };
package/dist/index.esm.js CHANGED
@@ -157,6 +157,61 @@ const builtinParamTypes = {
157
157
  },
158
158
  };
159
159
 
160
+ /**
161
+ * searchParams の正規化(docs/router-state-contract-design.md §3.5)。
162
+ *
163
+ * - 読み取り形状は `Record<string, string>`。`URLSearchParams` の生ハンドルは
164
+ * 露出しない(生ハンドルを state に入れない規範)。
165
+ * - キー重複(`?tag=a&tag=b`)は **last-wins**。
166
+ * - 値のデコードは `URLSearchParams` に委ねる(`+` → space を含む)。
167
+ * - 露出オブジェクトは freeze したスナップショット(消費側の変異は loud failure)。
168
+ */
169
+ function parseSearchParams(search) {
170
+ const result = {};
171
+ for (const [key, value] of new URLSearchParams(search)) {
172
+ result[key] = value;
173
+ }
174
+ return Object.freeze(result);
175
+ }
176
+ /**
177
+ * Record の shallow 比較。params の変化判定(§3.3: 文字列値の shallow 比較)と
178
+ * searchParams の変化判定(§3.5: キーをソートした pair 列の比較 = 順序非依存)に
179
+ * 共通で使う。
180
+ */
181
+ function shallowEqualRecords(a, b) {
182
+ const aKeys = Object.keys(a);
183
+ const bKeys = Object.keys(b);
184
+ if (aKeys.length !== bKeys.length)
185
+ return false;
186
+ for (const key of aKeys) {
187
+ if (!Object.prototype.hasOwnProperty.call(b, key))
188
+ return false;
189
+ if (a[key] !== b[key])
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * guard 判定関数に渡す第 3 引数を組み立てる。
197
+ *
198
+ * `<wcs-router>` が commit 後に露出する観測面(params / typedParams / searchParams /
199
+ * routeName)と同じ語彙・同じ正規化を、commit **前**の guard 相で読めるようにする。
200
+ * guard がデータをロードしてから進入を許可する(loader)とき、ロードに要る
201
+ * パラメータをここから取る。
202
+ *
203
+ * frozen スナップショット — 消費側の変異は loud failure(router 観測面と同じ規範)。
204
+ * routeName は最深マッチルートの name(fallback 時は fallback の name、無名は "")。
205
+ */
206
+ function createGuardContext(matchResult) {
207
+ return Object.freeze({
208
+ params: Object.freeze({ ...matchResult.params }),
209
+ typedParams: Object.freeze({ ...matchResult.typedParams }),
210
+ searchParams: parseSearchParams(matchResult.search ?? ""),
211
+ routeName: matchResult.routes[matchResult.routes.length - 1]?.name ?? "",
212
+ });
213
+ }
214
+
160
215
  const weights = {
161
216
  'static': 2,
162
217
  'param': 1,
@@ -475,6 +530,13 @@ class RouteCore extends EventTarget {
475
530
  this._guardHandlerLoadFailed = true;
476
531
  this._resolveSetGuardHandler?.();
477
532
  }
533
+ /**
534
+ * guard 相(components/types.ts の GuardResult を参照)。
535
+ * - 空でない文字列 → その絶対パスへ動的リダイレクト(`guard` 属性より優先)
536
+ * - falsy(false / undefined / null / "")→ `guard` 属性のパスへ
537
+ * - オブジェクト → 許可し、ロード済みデータとして返す(runGuardPhase が集約)
538
+ * - true → 許可(データ無し = null)
539
+ */
478
540
  async guardCheck(matchResult) {
479
541
  if (this._hasGuard && this._waitForSetGuardHandler) {
480
542
  await this._waitForSetGuardHandler;
@@ -482,15 +544,20 @@ class RouteCore extends EventTarget {
482
544
  if (this._guardHandler) {
483
545
  const toPath = matchResult.path;
484
546
  const fromPath = matchResult.lastPath;
485
- const allowed = await this._guardHandler(toPath, fromPath);
486
- if (!allowed) {
547
+ const result = await this._guardHandler(toPath, fromPath, createGuardContext(matchResult));
548
+ if (typeof result === 'string' && result !== '') {
549
+ throw new GuardCancel('Navigation cancelled by guard.', result);
550
+ }
551
+ if (!result) {
487
552
  throw new GuardCancel('Navigation cancelled by guard.', this._guardFallbackPath);
488
553
  }
554
+ return typeof result === 'object' ? result : null;
489
555
  }
490
556
  else if (this._hasGuard && this._guardHandlerLoadFailed) {
491
557
  // guardHandler のロードに失敗した場合は fallback パスへ
492
558
  throw new GuardCancel('Navigation cancelled: guard handler failed to load.', this._guardFallbackPath);
493
559
  }
560
+ return null;
494
561
  }
495
562
  }
496
563
 
@@ -704,6 +771,109 @@ class Route extends HTMLElement {
704
771
  }
705
772
  }
706
773
 
774
+ /**
775
+ * Trusted Types (`require-trusted-types-for 'script'`) 対応。正本は docs/csp.md §7。
776
+ *
777
+ * router が HTML sink に流すのは `<wcs-layout>` のテンプレート、つまり **作者が書いた
778
+ * マークアップ**(同一文書の `<template>` か `src` で取りに行くアプリの資産)であって、
779
+ * ユーザー入力ではない。Lit がテンプレートリテラルにだけ policy を当てているのと同じ
780
+ * 立て付けで、ここは identity policy で署名してよい層に当たる。
781
+ *
782
+ * policy 名は全 @wcstack パッケージで単一の `wcstack` に固定する。利用側の CSP に
783
+ * 書く行を 1 本に固定できるほうが、パッケージごとに名前を分けて最小権限にするより
784
+ * 導入摩擦が小さいため(署名対象がどれも作者制御の値なので、分割しても守るものが
785
+ * 増えない)。生成結果はグローバルスロットで共有し、複数パッケージが同居しても
786
+ * `createPolicy` は 1 回だけ走らせる — `trusted-types` ディレクティブがある場合、
787
+ * `'allow-duplicates'` 無しの重複生成は例外になるため。
788
+ *
789
+ * **利用側が注入した policy はここでは優先しない。** 注入口は「信頼できない値に対する
790
+ * sanitizer」として使われる想定で(fetch のレスポンス、state の値)、実際に案内している
791
+ * 設定も DOMPurify である。作者が書いたレイアウトを sanitizer に通すと、既定でカスタム
792
+ * 要素が除去されて `<wcs-link>` などがレイアウトから消える——例外も警告も無しに。
793
+ * よってここは identity policy を優先し、**それを作れなかったときだけ**利用側 policy に
794
+ * 落ちる(その場合は 1 度警告する)。
795
+ */
796
+ /** 利用側が policy を差し込むグローバルスロット(全 @wcstack パッケージ共通)。 */
797
+ const TRUSTED_TYPES_POLICY_SLOT = Symbol.for("wcstack.trustedTypes.policy");
798
+ /** wcstack が生成した identity policy を共有するスロット(内部用)。 */
799
+ const INTERNAL_POLICY_SLOT = Symbol.for("wcstack.trustedTypes.internal");
800
+ const POLICY_NAME = "wcstack";
801
+ /** 利用側が注入した policy。 */
802
+ function getTrustedTypesPolicy() {
803
+ const value = globalThis[TRUSTED_TYPES_POLICY_SLOT];
804
+ if (value === null || typeof value !== "object")
805
+ return null;
806
+ return value;
807
+ }
808
+ /** 利用側 policy を設定する(`null` で解除)。 */
809
+ function setTrustedTypesPolicy(policy) {
810
+ globalThis[TRUSTED_TYPES_POLICY_SLOT] = policy;
811
+ }
812
+ /**
813
+ * 作者制御の文字列に署名するための共有 identity policy。TT 非対応ブラウザでは
814
+ * null(呼び出し側は生文字列のまま進む)。生成失敗も null をキャッシュして、
815
+ * 診断は 1 度だけ出す。
816
+ */
817
+ function getInternalPolicy() {
818
+ const holder = globalThis;
819
+ const cached = holder[INTERNAL_POLICY_SLOT];
820
+ if (cached !== undefined)
821
+ return cached;
822
+ const factory = globalThis.trustedTypes;
823
+ let policy = null;
824
+ if (factory && typeof factory.createPolicy === "function") {
825
+ try {
826
+ policy = factory.createPolicy(POLICY_NAME, {
827
+ createHTML: (input) => input,
828
+ createScriptURL: (input) => input,
829
+ });
830
+ }
831
+ catch (error) {
832
+ // 利用側 policy に落ちられるなら、報告は初回使用時の 1 回の warn(trustAuthoredHTML)
833
+ // に任せる — docs/csp.md §7 の「フォールバックと一度きりの warn」。error まで
834
+ // 出すと、既に policy を注入した利用者へ「注入せよ」と言うことになる(実 Chromium の
835
+ // e2e で確認)。落ちる先が無いときだけ、直し方付きで error にする
836
+ if (typeof getTrustedTypesPolicy()?.createHTML !== "function") {
837
+ console.error(`[@wcstack/router] Could not create the Trusted Types policy "${POLICY_NAME}". `
838
+ + `Allow it in the CSP (\`trusted-types ${POLICY_NAME};\`), or inject your own policy `
839
+ + `at globalThis[Symbol.for("wcstack.trustedTypes.policy")]. See docs/csp.md section 7.`, error);
840
+ }
841
+ }
842
+ }
843
+ holder[INTERNAL_POLICY_SLOT] = policy;
844
+ return policy;
845
+ }
846
+ let _fallbackWarned = false;
847
+ /**
848
+ * 作者が書いたマークアップを TrustedHTML に変換する。
849
+ *
850
+ * 解決順は 共有 identity policy > (TT はあるが identity policy を作れなかった場合のみ)
851
+ * 利用側 policy > 生文字列。TT 非対応ブラウザでは署名自体が不要なので、利用側 policy が
852
+ * 入っていても素通しする——作者のレイアウトを sanitizer に通す理由はどこにも無い。
853
+ */
854
+ function trustAuthoredHTML(html) {
855
+ const internal = getInternalPolicy();
856
+ const internalCreateHTML = internal?.createHTML;
857
+ if (typeof internalCreateHTML === "function") {
858
+ return internalCreateHTML.call(internal, html);
859
+ }
860
+ if (!("trustedTypes" in globalThis))
861
+ return html;
862
+ const adopted = getTrustedTypesPolicy();
863
+ const adoptedCreateHTML = adopted?.createHTML;
864
+ if (typeof adoptedCreateHTML === "function") {
865
+ if (!_fallbackWarned) {
866
+ _fallbackWarned = true;
867
+ console.warn(`[@wcstack/router] Falling back to the injected Trusted Types policy to expand a layout `
868
+ + `template, because the "${POLICY_NAME}" policy could not be created. If that policy `
869
+ + `sanitizes (e.g. DOMPurify), custom elements in the layout may be stripped. `
870
+ + `Allow \`trusted-types ${POLICY_NAME};\` in the CSP to avoid this. See docs/csp.md section 7.`);
871
+ }
872
+ return adoptedCreateHTML.call(adopted, html);
873
+ }
874
+ return html;
875
+ }
876
+
707
877
  const cache = new Map();
708
878
  class Layout extends HTMLElement {
709
879
  _uuid = getUUID();
@@ -741,19 +911,22 @@ class Layout extends HTMLElement {
741
911
  console.warn(`${config.tagNames.layout} have both "src" and "layout" attributes.`);
742
912
  }
743
913
  const template = document.createElement('template');
914
+ // Trusted Types: ここに流れるのは作者が書いたレイアウトのマークアップなので、
915
+ // 共有 identity policy で署名してよい層(docs/csp.md §7)。利用側が独自 policy を
916
+ // 注入していればそちらが優先される。
744
917
  if (source) {
745
918
  if (cache.has(source)) {
746
- template.innerHTML = cache.get(source) || '';
919
+ template.innerHTML = trustAuthoredHTML(cache.get(source) || '');
747
920
  }
748
921
  else {
749
922
  // _loadTemplateFromSource は内部で cache.set を実行する
750
- template.innerHTML = await this._loadTemplateFromSource(source) || '';
923
+ template.innerHTML = trustAuthoredHTML(await this._loadTemplateFromSource(source) || '');
751
924
  }
752
925
  }
753
926
  else if (layoutId) {
754
927
  const templateContent = this._loadTemplateFromDocument(layoutId);
755
928
  if (templateContent) {
756
- template.innerHTML = templateContent;
929
+ template.innerHTML = trustAuthoredHTML(templateContent);
757
930
  }
758
931
  else {
759
932
  console.warn(`${config.tagNames.layout} could not find template with id "${layoutId}".`);
@@ -1237,7 +1410,10 @@ async function _parseNode(routerNode, node, routes, routesByPath) {
1237
1410
  element = cloneElement;
1238
1411
  }
1239
1412
  const children = await _parseNode(routerNode, element, routes, routesByPath);
1240
- element.innerHTML = "";
1413
+ // 空文字の innerHTML 代入は Trusted Types 下でも通る実装が多いが、その細目に
1414
+ // 寄りかからずノード操作で書く(docs/csp.md §7)。意図としても「子を全消しして
1415
+ // 差し替える」のほうが直接的。
1416
+ element.replaceChildren();
1241
1417
  element.appendChild(children);
1242
1418
  fragment.appendChild(appendNode);
1243
1419
  }
@@ -1875,9 +2051,17 @@ function bindRouteContent(route) {
1875
2051
  */
1876
2052
  async function runGuardPhase(routerNode, matchResult) {
1877
2053
  try {
2054
+ // guard がオブジェクトを返したルートのデータを親→子の順で浅くマージし、
2055
+ // matchResult.data に載せる(applyRoute / SSR 採用が commit へ運ぶ)。
2056
+ // 何も返らなければ null — 「このナビゲーションにデータ無し」を明示する
2057
+ let data = null;
1878
2058
  for (const route of matchResult.routes) {
1879
- await route.guardCheck(matchResult);
2059
+ const routeData = await route.guardCheck(matchResult);
2060
+ if (routeData) {
2061
+ data = data === null ? routeData : Object.assign({}, data, routeData);
2062
+ }
1880
2063
  }
2064
+ matchResult.data = data;
1881
2065
  }
1882
2066
  catch (e) {
1883
2067
  if (e instanceof GuardCancel) {
@@ -1989,6 +2173,8 @@ async function applyRoute(routerNode, outlet, fullPath, lastPath, search = "") {
1989
2173
  params: routerNode.params,
1990
2174
  typedParams: routerNode.typedParams,
1991
2175
  routeName: routerNode.routeName,
2176
+ // guard 相を通らないので data も据え置き(同一性を保ち data-changed を発火させない)
2177
+ data: routerNode.data,
1992
2178
  search,
1993
2179
  path,
1994
2180
  });
@@ -2010,6 +2196,8 @@ async function applyRoute(routerNode, outlet, fullPath, lastPath, search = "") {
2010
2196
  }
2011
2197
  }
2012
2198
  matchResult.lastPath = lastPath;
2199
+ // guard 相の第 3 引数(IGuardContext.searchParams)が commit と同じクエリを読めるように供給する
2200
+ matchResult.search = search;
2013
2201
  const lastRoutes = outlet.lastRoutes;
2014
2202
  const committed = await showRouteContent(routerNode, matchResult, lastRoutes);
2015
2203
  // GuardCancel により中断された場合は state を更新しない
@@ -2022,6 +2210,8 @@ async function applyRoute(routerNode, outlet, fullPath, lastPath, search = "") {
2022
2210
  params: matchResult.params,
2023
2211
  typedParams: matchResult.typedParams,
2024
2212
  routeName: matchResult.routes[matchResult.routes.length - 1]?.name ?? "",
2213
+ // guard 相(runGuardPhase)が集めたロード済みデータ。無ければ null に戻る
2214
+ data: matchResult.data ?? null,
2025
2215
  search,
2026
2216
  path,
2027
2217
  });
@@ -2047,41 +2237,6 @@ function getNavigation() {
2047
2237
  return nav;
2048
2238
  }
2049
2239
 
2050
- /**
2051
- * searchParams の正規化(docs/router-state-contract-design.md §3.5)。
2052
- *
2053
- * - 読み取り形状は `Record<string, string>`。`URLSearchParams` の生ハンドルは
2054
- * 露出しない(生ハンドルを state に入れない規範)。
2055
- * - キー重複(`?tag=a&tag=b`)は **last-wins**。
2056
- * - 値のデコードは `URLSearchParams` に委ねる(`+` → space を含む)。
2057
- * - 露出オブジェクトは freeze したスナップショット(消費側の変異は loud failure)。
2058
- */
2059
- function parseSearchParams(search) {
2060
- const result = {};
2061
- for (const [key, value] of new URLSearchParams(search)) {
2062
- result[key] = value;
2063
- }
2064
- return Object.freeze(result);
2065
- }
2066
- /**
2067
- * Record の shallow 比較。params の変化判定(§3.3: 文字列値の shallow 比較)と
2068
- * searchParams の変化判定(§3.5: キーをソートした pair 列の比較 = 順序非依存)に
2069
- * 共通で使う。
2070
- */
2071
- function shallowEqualRecords(a, b) {
2072
- const aKeys = Object.keys(a);
2073
- const bKeys = Object.keys(b);
2074
- if (aKeys.length !== bKeys.length)
2075
- return false;
2076
- for (const key of aKeys) {
2077
- if (!Object.prototype.hasOwnProperty.call(b, key))
2078
- return false;
2079
- if (a[key] !== b[key])
2080
- return false;
2081
- }
2082
- return true;
2083
- }
2084
-
2085
2240
  function splitUrlTarget(to) {
2086
2241
  let rest = to;
2087
2242
  let hash = "";
@@ -2212,6 +2367,9 @@ class Router extends HTMLElement {
2212
2367
  getter: (e) => e.detail.typedParams },
2213
2368
  { name: "searchParams", event: "wcs-router:search-changed", semantics: "state" },
2214
2369
  { name: "routeName", event: "wcs-router:route-name-changed", semantics: "state" },
2370
+ // guard 関数がオブジェクトを返したナビゲーションのロード済みデータ(loader)。
2371
+ // output-only。データ無しのナビゲーションでは null に戻る
2372
+ { name: "data", event: "wcs-router:data-changed", semantics: "state" },
2215
2373
  ],
2216
2374
  // `navigateUrl` は observable output であると同時に settable な書き込み面でもある
2217
2375
  // (setter が navigate() を起動し、完了後に自分で null へ戻す)。properties にだけ
@@ -2249,6 +2407,9 @@ class Router extends HTMLElement {
2249
2407
  _typedParams = EMPTY_RECORD;
2250
2408
  _searchParams = EMPTY_RECORD;
2251
2409
  _routeName = '';
2410
+ // guard 相のロード済みデータ。frozen にしない — 作者が返したオブジェクトをそのまま
2411
+ // 露出する(state 側で保持・変異されうる作者所有の値。params とは所有者が違う)
2412
+ _data = null;
2252
2413
  /** 最初の成功 commit を通過したか(§4.4 の初回ガード) */
2253
2414
  _hasCommitted = false;
2254
2415
  _connectedCallbackPromise;
@@ -2414,6 +2575,9 @@ class Router extends HTMLElement {
2414
2575
  get routeName() {
2415
2576
  return this._routeName;
2416
2577
  }
2578
+ get data() {
2579
+ return this._data;
2580
+ }
2417
2581
  /**
2418
2582
  * same-match 判定(docs/router-state-contract-design.md §4.4)。
2419
2583
  *
@@ -2435,14 +2599,19 @@ class Router extends HTMLElement {
2435
2599
  *
2436
2600
  * 全内部値を先にコミットし、その後で初めてイベントを発火する — どのイベントの
2437
2601
  * リスナーから要素プロパティを読んでも、遷移後スナップショットの一貫した値が
2438
- * 見える。発火順序は params → route-name → search → path。`path` を最後に
2439
- * 置くのは、既存例で `path` が「ナビゲーション完了」の信号として使われている
2440
- * ため。各イベントは変化した commit のみ発火する。
2602
+ * 見える。発火順序は data → params → route-name → search → path。`data`
2603
+ * 先頭に置くのは、params のリスナーがロード済みデータを読めるようにするため。
2604
+ * `path` を最後に置くのは、既存例で `path` が「ナビゲーション完了」の信号として
2605
+ * 使われているため。各イベントは変化した commit のみ発火する。
2441
2606
  */
2442
2607
  commitNavigation(commit) {
2443
2608
  const nextParams = Object.freeze({ ...commit.params });
2444
2609
  const nextTypedParams = Object.freeze({ ...commit.typedParams });
2445
2610
  const nextSearchParams = parseSearchParams(commit.search);
2611
+ // data は同一性で比較する(guard は遷移ごとに新しいオブジェクトを返す。
2612
+ // same-match は前の参照をそのまま渡すので発火しない)
2613
+ const nextData = commit.data ?? null;
2614
+ const dataChanged = this._data !== nextData;
2446
2615
  const paramsChanged = !shallowEqualRecords(this._params, nextParams);
2447
2616
  const routeNameChanged = this._routeName !== commit.routeName;
2448
2617
  const searchChanged = !shallowEqualRecords(this._searchParams, nextSearchParams);
@@ -2450,6 +2619,9 @@ class Router extends HTMLElement {
2450
2619
  // --- 先に全内部値をコミット ---
2451
2620
  // 変化した面だけ差し替える(ナビゲーションごとに新しいオブジェクトになるので
2452
2621
  // state の same-value guard を正しく通過する。不変の面は同一性を保つ)。
2622
+ if (dataChanged) {
2623
+ this._data = nextData;
2624
+ }
2453
2625
  if (paramsChanged) {
2454
2626
  this._params = nextParams;
2455
2627
  this._typedParams = nextTypedParams;
@@ -2462,7 +2634,13 @@ class Router extends HTMLElement {
2462
2634
  }
2463
2635
  this._path = commit.path;
2464
2636
  this._hasCommitted = true;
2465
- // --- その後で発火(順序規範: params → route-name → search → path) ---
2637
+ // --- その後で発火(順序規範: data → params → route-name → search → path) ---
2638
+ if (dataChanged) {
2639
+ this.dispatchEvent(new CustomEvent("wcs-router:data-changed", {
2640
+ detail: this._data,
2641
+ bubbles: true,
2642
+ }));
2643
+ }
2466
2644
  if (paramsChanged) {
2467
2645
  this.dispatchEvent(new CustomEvent("wcs-router:params-changed", {
2468
2646
  detail: { params: this._params, typedParams: this._typedParams },
@@ -2955,6 +3133,7 @@ class Router extends HTMLElement {
2955
3133
  params: matchResult.params,
2956
3134
  typedParams: matchResult.typedParams,
2957
3135
  routeName: matchResult.routes[matchResult.routes.length - 1].name,
3136
+ data: matchResult.data ?? null,
2958
3137
  search: window.location.search || "",
2959
3138
  path,
2960
3139
  });
@@ -3678,11 +3857,11 @@ function bootstrapRouter(config, registry) {
3678
3857
  registerComponents(registry);
3679
3858
  }
3680
3859
 
3681
- var version = "2.1.1";
3860
+ var version = "2.3.0";
3682
3861
  var pkg = {
3683
3862
  version: version};
3684
3863
 
3685
3864
  const VERSION = pkg.version;
3686
3865
 
3687
- export { Route, RouteCore, Router, VERSION, bootstrapRouter, getConfig };
3866
+ export { Route, RouteCore, Router, TRUSTED_TYPES_POLICY_SLOT, VERSION, bootstrapRouter, getConfig, getTrustedTypesPolicy, setTrustedTypesPolicy };
3688
3867
  //# sourceMappingURL=index.esm.js.map