@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/README.ja.md +88 -7
- package/README.md +88 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +86 -9
- package/dist/index.esm.js +227 -48
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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。`
|
|
317
|
-
*
|
|
318
|
-
*
|
|
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<
|
|
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
|
-
|
|
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
|
|
486
|
-
if (
|
|
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
|
-
|
|
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。`
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
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.
|
|
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
|