soseki 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.config/_is-debug-mode.ts +9 -0
  2. package/.config/dependency-cruiser.cjs +408 -0
  3. package/.config/env.d.ts +8 -0
  4. package/.config/mise.toml +28 -0
  5. package/.config/navigation-api.d.ts +18 -0
  6. package/.config/tsconfig.build.json +28 -0
  7. package/.config/tsconfig.config.json +21 -0
  8. package/.config/tsconfig.test.json +26 -0
  9. package/.config/tsconfig.web.json +26 -0
  10. package/.config/vitest.client.ts +30 -0
  11. package/package.json +45 -0
  12. package/src/components/action-id.tsx +35 -0
  13. package/src/components/browser-router.tsx +31 -0
  14. package/src/components/hidden-input.tsx +39 -0
  15. package/src/components/outlet.tsx +17 -0
  16. package/src/components/router.tsx +234 -0
  17. package/src/contexts/route-context.ts +21 -0
  18. package/src/contexts/router-context.ts +55 -0
  19. package/src/core/_action-id-registry.ts +11 -0
  20. package/src/core/_capture-stack-trace.ts +12 -0
  21. package/src/core/_compare-route-paths.ts +90 -0
  22. package/src/core/_create-html-form-element-form-form-data.ts +32 -0
  23. package/src/core/_encode-pathname.ts +17 -0
  24. package/src/core/_is-error.ts +16 -0
  25. package/src/core/_is-promise-like.ts +14 -0
  26. package/src/core/_match-route-path.ts +39 -0
  27. package/src/core/_process-routes.ts +56 -0
  28. package/src/core/_singleton.ts +45 -0
  29. package/src/core/_unreachable.ts +22 -0
  30. package/src/core/_use-singleton.ts +24 -0
  31. package/src/core/_valibot.ts +147 -0
  32. package/src/core/_weak-id-registry.ts +125 -0
  33. package/src/core/constants.ts +4 -0
  34. package/src/core/data-map.types.ts +28 -0
  35. package/src/core/data-store.types.ts +25 -0
  36. package/src/core/deferred-promise.ts +408 -0
  37. package/src/core/errors.ts +680 -0
  38. package/src/core/expect-history-entry.ts +95 -0
  39. package/src/core/history-entry-id-schema.ts +27 -0
  40. package/src/core/history-entry-url-schema.ts +35 -0
  41. package/src/core/init-loaders.ts +79 -0
  42. package/src/core/match-routes.ts +91 -0
  43. package/src/core/readonly-form-data.types.ts +63 -0
  44. package/src/core/readonly-url.types.ts +156 -0
  45. package/src/core/redirect-response.ts +36 -0
  46. package/src/core/route-request.ts +92 -0
  47. package/src/core/route.types.ts +351 -0
  48. package/src/core/start-actions.ts +274 -0
  49. package/src/core/start-loaders.ts +254 -0
  50. package/src/core.ts +43 -0
  51. package/src/engines/engine.types.ts +216 -0
  52. package/src/engines/navigation-api-engine.ts +406 -0
  53. package/src/hooks/_use-route-context.ts +19 -0
  54. package/src/hooks/_use-router-context.ts +25 -0
  55. package/src/hooks/use-action-data.ts +37 -0
  56. package/src/hooks/use-loader-data.ts +28 -0
  57. package/src/hooks/use-navigate.ts +64 -0
  58. package/src/hooks/use-params.ts +11 -0
  59. package/src/hooks/use-pathname.ts +10 -0
  60. package/src/hooks/use-submit.ts +111 -0
  61. package/src/soseki.ts +75 -0
  62. package/src/utils/get-action-id.ts +12 -0
  63. package/src/utils/href.ts +17 -0
  64. package/src/utils/redirect.ts +13 -0
  65. package/src/utils/route-index.ts +70 -0
  66. package/src/utils/route-route.ts +111 -0
  67. package/src/utils/set-action-id.ts +14 -0
  68. package/tests/core/_capture-stack-trace.test.ts +46 -0
  69. package/tests/core/_compare-route-paths.test.ts +134 -0
  70. package/tests/core/_encode-pathname.test.ts +77 -0
  71. package/tests/core/_is-error.test.ts +108 -0
  72. package/tests/core/_is-promise-like.test.ts +100 -0
  73. package/tests/core/_match-route-path.test.ts +74 -0
  74. package/tests/core/_process-routes.test.ts +146 -0
  75. package/tests/core/_singleton.test.ts +102 -0
  76. package/tests/core/_unreachable.test.ts +38 -0
  77. package/tests/core/_use-singleton.test.ts +47 -0
  78. package/tests/core/_weak-id-registry.test.ts +137 -0
  79. package/tests/core/deferred-promise.test.ts +218 -0
  80. package/tests/core/expect-history-entry.test.ts +112 -0
  81. package/tests/core/init-loaders.test.ts +172 -0
  82. package/tests/core/match-routes.test.ts +178 -0
  83. package/tests/core/redirect-response.test.ts +36 -0
  84. package/tests/core/route-request.test.ts +93 -0
  85. package/tests/core/start-actions.test.ts +319 -0
  86. package/tests/core/start-loaders.test.ts +276 -0
  87. package/tests/engines/navigation-api-engine.test.ts +162 -0
  88. package/tsconfig.json +7 -0
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import actionIdRegistry from "../core/_action-id-registry.js";
3
+ import { ACTION_ID_FORM_DATA_NAME } from "../core/constants.js";
4
+ import type { IAction } from "../core/route.types.js";
5
+ import HiddenInput from "./hidden-input.jsx";
6
+
7
+ /**
8
+ * ActionId コンポーネントのプロパティーを定義する型です。
9
+ */
10
+ export type ActionIdProps = {
11
+ /**
12
+ * 実行対象となるアクション関数です。
13
+ */
14
+ action: IAction;
15
+ };
16
+
17
+ /**
18
+ * アクションに対応する識別子を隠しフィールドとしてレンダリングするコンポーネントです。
19
+ *
20
+ * アクション関数をレジストリーに登録し、発行された ID をフォームデータとして送信可能な状態にします。
21
+ */
22
+ export default /*#__PURE__*/ React.memo(
23
+ function ActionId(props: ActionIdProps): React.ReactElement {
24
+ const { action } = props;
25
+ const id = actionIdRegistry.set(action);
26
+
27
+ return (
28
+ <HiddenInput
29
+ name={ACTION_ID_FORM_DATA_NAME}
30
+ value={id}
31
+ />
32
+ );
33
+ },
34
+ (a, b) => a.action === b.action,
35
+ );
@@ -0,0 +1,31 @@
1
+ import useSingleton from "../core/_use-singleton.js";
2
+ import type { RouteDefinition } from "../core/route.types.js";
3
+ import NavigationApiEngine from "../engines/navigation-api-engine.js";
4
+ import Router from "./router.jsx";
5
+
6
+ /**
7
+ * BrowserRouter コンポーネントに渡されるプロパティーの型定義です。
8
+ */
9
+ export type BrowserRouterProps = {
10
+ /**
11
+ * アプリケーション全体のルート定義を格納した配列です。
12
+ */
13
+ routes: readonly RouteDefinition[];
14
+ };
15
+
16
+ /**
17
+ * ブラウザー標準の Navigation API を使用してルーティングを行うためのコンポーネントです。
18
+ *
19
+ * `NavigationApiEngine` のインスタンスをシングルトンとして保持し、ルーターの基盤を提供します。
20
+ */
21
+ export default function BrowserRouter(props: BrowserRouterProps): React.ReactElement {
22
+ const { routes } = props;
23
+ const engine = useSingleton(() => new NavigationApiEngine());
24
+
25
+ return (
26
+ <Router
27
+ engine={engine}
28
+ routes={routes}
29
+ />
30
+ );
31
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * 隠し入力フィールド(hidden input)のプロパティーを定義する型です。
5
+ */
6
+ export type HiddenInputProps = {
7
+ /**
8
+ * フォーム送信時に使用される要素の名前です。
9
+ */
10
+ name: string;
11
+
12
+ /**
13
+ * フォーム送信時に送信される値です。
14
+ */
15
+ value: string;
16
+ };
17
+
18
+ /**
19
+ * フォーム内で非表示の状態を保持するための隠し入力コンポーネントです。
20
+ */
21
+ export default /*#__PURE__*/ React.memo(
22
+ function HiddenInput(props: HiddenInputProps) {
23
+ const {
24
+ name,
25
+ value,
26
+ } = props;
27
+
28
+ return (
29
+ <input
30
+ type="hidden"
31
+ name={name}
32
+ style={{ display: "none" }}
33
+ value={value}
34
+ hidden
35
+ />
36
+ );
37
+ },
38
+ (a, b) => a.name === b.name && a.value === b.value,
39
+ );
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import RouteContext from "../contexts/route-context.js";
3
+ import { RouteContextMissingError } from "../core/errors.js";
4
+
5
+ /**
6
+ * 現在のルート階層における子ルート(アウトレット)を描画するためのコンポーネントです。
7
+ *
8
+ * ネストされたルーティングにおいて、親ルートのコンポーネント内で子ルートの挿入位置を指定するために使用します。
9
+ */
10
+ export default function Outlet(): React.ReactElement | null {
11
+ const routeContext = React.use(RouteContext);
12
+ if (!routeContext) {
13
+ throw new RouteContextMissingError();
14
+ }
15
+
16
+ return routeContext.outlet;
17
+ }
@@ -0,0 +1,234 @@
1
+ import * as React from "react";
2
+ import RouteContext from "../contexts/route-context.js";
3
+ import RouterContext, {
4
+ type RouterContextValue,
5
+ type RouterRef,
6
+ } from "../contexts/router-context.js";
7
+ import processRoutes from "../core/_process-routes.js";
8
+ import type { IDataStore } from "../core/data-store.types.js";
9
+ import type { HistoryEntry } from "../core/expect-history-entry.js";
10
+ import type { MatchedRoute } from "../core/match-routes.js";
11
+ import type { IAction, ILoader, RouteDefinition } from "../core/route.types.js";
12
+ import type { IEngine, RouterState, SubmitArgs } from "../engines/engine.types.js";
13
+ import type { NavigateArgs } from "../engines/engine.types.js";
14
+
15
+ /**
16
+ * ComponentRenderer コンポーネントに渡されるプロパティーの型定義です。
17
+ */
18
+ type ComponentRendererProps = {
19
+ /**
20
+ * 現在のコンテキストでレンダリングされる、マッチしたルート情報です。
21
+ */
22
+ route: MatchedRoute;
23
+
24
+ /**
25
+ * ネストされた下位のルート要素です。存在しない場合は null を指定します。
26
+ */
27
+ outlet: React.ReactElement<RouteRendererProps, typeof RouteRenderer> | null;
28
+ };
29
+
30
+ /**
31
+ * 個別のルートコンポーネントをレンダリングし、ルートコンテキストを提供する内部コンポーネントです。
32
+ */
33
+ function ComponentRenderer(props: ComponentRendererProps): React.JSX.Element | null {
34
+ const {
35
+ route: {
36
+ path,
37
+ index,
38
+ params,
39
+ urlPath,
40
+ component: Component,
41
+ },
42
+ outlet,
43
+ } = props;
44
+ const context = {
45
+ path,
46
+ index,
47
+ params,
48
+ outlet,
49
+ urlPath,
50
+ };
51
+
52
+ return (
53
+ <RouteContext value={context}>
54
+ {Component ? <Component /> : outlet}
55
+ </RouteContext>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * マッチしたルートを再帰的にレンダリングするためのプロパティーです。
61
+ */
62
+ type RouteRendererProps = {
63
+ /**
64
+ * 現在のパスにマッチしたルートの配列です。
65
+ */
66
+ routes: readonly MatchedRoute[];
67
+
68
+ /**
69
+ * 現在レンダリング対象となっているルートのインデックス番号です。
70
+ */
71
+ index?: number;
72
+ };
73
+
74
+ /**
75
+ * マッチしたルートの階層を順番にレンダリングする内部コンポーネントです。
76
+ */
77
+ function RouteRenderer(props: RouteRendererProps): React.ReactElement {
78
+ const {
79
+ index = 0,
80
+ routes,
81
+ } = props;
82
+ const route = routes[index]!;
83
+ const outlet = index < routes.length - 1
84
+ ? <RouteRenderer routes={routes} index={index + 1} />
85
+ : null;
86
+
87
+ return (
88
+ <ComponentRenderer
89
+ route={route}
90
+ outlet={outlet}
91
+ />
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Router コンポーネントに渡されるプロパティーの型定義です。
97
+ */
98
+ export type RouterProps = {
99
+ /**
100
+ * ルーティングのロジックを制御するエンジンインスタンスです。
101
+ */
102
+ engine: IEngine;
103
+
104
+ /**
105
+ * アプリケーションのルート定義の配列です。
106
+ */
107
+ routes: readonly RouteDefinition[];
108
+ };
109
+
110
+ /**
111
+ * ルーティング機能を提供するメインのコンポーネントです。
112
+ *
113
+ * エンジンの管理、状態の同期、およびルートのレンダリングを担います。
114
+ */
115
+ export default function Router(props: RouterProps) {
116
+ const {
117
+ engine,
118
+ routes: routesProp,
119
+ } = props;
120
+ const routerRef = React.useRef({} as {
121
+ submit(args: SubmitArgs): void;
122
+ navigate(args: NavigateArgs): void;
123
+ currentEntry: HistoryEntry;
124
+ actionDataStore: IDataStore<IAction>;
125
+ loaderDataStore: IDataStore<ILoader>;
126
+ });
127
+ const router = React.useMemo<{
128
+ start(): () => void;
129
+ context: RouterContextValue;
130
+ getRoutes(): readonly MatchedRoute[] | undefined;
131
+ }>(
132
+ () => {
133
+ const actionDataStore: IDataStore<IAction> = new Map();
134
+ const loaderDataStore: IDataStore<ILoader> = new Map();
135
+ const subscribers = new Set<() => void>();
136
+ const routes = processRoutes(routesProp);
137
+ let ac: AbortController | null = null;
138
+
139
+ function getAbortSignal(): AbortSignal {
140
+ return (ac ||= new AbortController()).signal;
141
+ }
142
+
143
+ const initialState = engine.init({
144
+ routes,
145
+ getSignal: getAbortSignal,
146
+ loaderDataStore,
147
+ });
148
+ let currentRoutes = initialState?.routes;
149
+
150
+ function updateRouter(newState?: RouterState | null) {
151
+ if (newState !== undefined) {
152
+ currentRoutes = newState?.routes;
153
+ }
154
+ if (newState) {
155
+ routerRef.current.currentEntry = newState.entry;
156
+ }
157
+
158
+ // 登録されているすべてのサブスクライバーに通知します。
159
+ subscribers.forEach(notify => notify());
160
+ }
161
+
162
+ function startRouterEngine() {
163
+ const stop = engine.start({
164
+ routes,
165
+ update: updateRouter,
166
+ getSignal: getAbortSignal,
167
+ actionDataStore,
168
+ loaderDataStore,
169
+ });
170
+
171
+ return function stopRouterEngine() {
172
+ try {
173
+ // エンジンの動作を停止します。
174
+ stop?.();
175
+ } finally {
176
+ // 進行中のすべての非同期処理を中断します。
177
+ try {
178
+ ac?.abort();
179
+ } catch {
180
+ }
181
+
182
+ // 中断後は次回の呼び出しに備えてコントローラーを破棄します。
183
+ ac = null;
184
+ }
185
+ };
186
+ }
187
+
188
+ Object.assign<RouterRef["current"], RouterRef["current"]>(routerRef.current, {
189
+ submit(args) {
190
+ return engine.submit(args);
191
+ },
192
+ navigate(args) {
193
+ return engine.navigate(args);
194
+ },
195
+ currentEntry: initialState?.entry!,
196
+ actionDataStore,
197
+ loaderDataStore,
198
+ });
199
+
200
+ return {
201
+ start: startRouterEngine,
202
+ context: {
203
+ routerRef,
204
+ subscribe(cb) {
205
+ subscribers.add(cb);
206
+ return () => {
207
+ subscribers.delete(cb);
208
+ };
209
+ },
210
+ },
211
+ getRoutes() {
212
+ return currentRoutes;
213
+ },
214
+ };
215
+ },
216
+ [
217
+ engine,
218
+ routesProp,
219
+ ],
220
+ );
221
+
222
+ React.useEffect(router.start, [router]);
223
+
224
+ const routes = React.useSyncExternalStore(router.context.subscribe, router.getRoutes);
225
+ if (!routes) {
226
+ return null;
227
+ }
228
+
229
+ return (
230
+ <RouterContext value={router.context}>
231
+ <RouteRenderer routes={routes.toReversed()} />
232
+ </RouterContext>
233
+ );
234
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import type { MatchedRoute } from "../core/match-routes.js";
3
+
4
+ /**
5
+ * 各ルートのコンテキストで共有される値の型定義です。
6
+ */
7
+ export type RouteContextValue = Pick<MatchedRoute, "path" | "index" | "params" | "urlPath"> & {
8
+ /**
9
+ * 子ルートを描画するための React 要素です。
10
+ *
11
+ * 子ルートが存在しない場合は `null` となります。
12
+ */
13
+ readonly outlet: React.ReactElement | null;
14
+ };
15
+
16
+ /**
17
+ * 個別のルート情報やアウトレット(子ルートの挿入場所)を保持するための React コンテキストです。
18
+ */
19
+ const RouteContext = /*#__PURE__*/ React.createContext<RouteContextValue | null>(null);
20
+
21
+ export default RouteContext;
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import type { IReadonlyDataStore } from "../core/data-store.types.js";
3
+ import type { HistoryEntry } from "../core/expect-history-entry.js";
4
+ import type { IAction, ILoader } from "../core/route.types.js";
5
+ import type { NavigateArgs, SubmitArgs } from "../engines/engine.types.js";
6
+
7
+ export type RouterRef = Readonly<
8
+ React.RefObject<{
9
+ /**
10
+ * フォームデータやクエリー パラメーターを送信します。
11
+ *
12
+ * @param args 送信内容と送信先を含む引数です。
13
+ */
14
+ readonly submit: (args: SubmitArgs) => void;
15
+
16
+ /**
17
+ * 指定されたパスへ遷移します。
18
+ *
19
+ * @param args 遷移先と遷移オプションを含む引数です。
20
+ */
21
+ readonly navigate: (args: NavigateArgs) => void;
22
+
23
+ /**
24
+ * 現在の履歴エントリー情報(ID、URL、インデックスなど)です。
25
+ */
26
+ readonly currentEntry: HistoryEntry;
27
+
28
+ /**
29
+ * 履歴 ID ごとに管理されているアクションデータのマップです。
30
+ */
31
+ readonly actionDataStore: IReadonlyDataStore<IAction>;
32
+
33
+ /**
34
+ * 履歴 ID ごとに管理されているローダーデータのマップです。
35
+ */
36
+ readonly loaderDataStore: IReadonlyDataStore<ILoader>;
37
+ }>
38
+ >;
39
+
40
+ /**
41
+ * ルーターコンテキストで共有される値の型定義です。
42
+ */
43
+ export type RouterContextValue = {
44
+ readonly routerRef: RouterRef;
45
+ readonly subscribe: (onRouterChange: () => void) => () => void;
46
+ };
47
+
48
+ /**
49
+ * ルーターの状態や操作関数をコンポーネントツリー全体で共有するための React コンテキストです。
50
+ *
51
+ * 初期値は `null` であり、通常は Provider を介して値が提供されます。
52
+ */
53
+ const RouterContext = /*#__PURE__*/ React.createContext<RouterContextValue | null>(null);
54
+
55
+ export default RouterContext;
@@ -0,0 +1,11 @@
1
+ import WeakIdRegistry from "./_weak-id-registry.js";
2
+ import type { IAction } from "./route.types.js";
3
+
4
+ /**
5
+ * `IAction` 型のオブジェクトとその識別子を管理するための、弱い参照を用いたレジストリーです。
6
+ *
7
+ * ガベージコレクションを妨げずにアクションの識別子を保持するために使用されます。
8
+ */
9
+ const actionIdRegistry = new WeakIdRegistry<IAction>();
10
+
11
+ export default actionIdRegistry;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * オブジェクトにスタックトレースをキャプチャーします。
3
+ *
4
+ * @param targetObject スタックトレースのプロパティーを追加する対象のオブジェクトです。
5
+ * @param constructorOpt スタックトレースの開始地点としてマークする関数です。
6
+ */
7
+ export default function captureStackTrace(targetObject: object, constructorOpt: Function): void {
8
+ // 実行環境(V8 エンジンなど)が Error.captureStackTrace をサポートしているか確認します。
9
+ if ("captureStackTrace" in Error && typeof Error.captureStackTrace === "function") {
10
+ Error.captureStackTrace(targetObject, constructorOpt);
11
+ }
12
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * セグメントの種類に応じたスコア定数です。
3
+ *
4
+ * 数値が大きいほど優先順位(具体性)が高いです。
5
+ */
6
+ const SCORE = {
7
+ /**
8
+ * 静的なパスセグメントのスコアです。
9
+ */
10
+ STATIC: 4,
11
+
12
+ /**
13
+ * 必須のパラメーターセグメントのスコアです。
14
+ */
15
+ PARAMETER: 3,
16
+
17
+ /**
18
+ * 任意のパラメーターセグメントのスコアです。
19
+ */
20
+ OPTIONAL_PARAM: 2,
21
+
22
+ /**
23
+ * ワイルドカードセグメントのスコアです。
24
+ */
25
+ WILDCARD: 1,
26
+ } as const;
27
+
28
+ /**
29
+ * 指定されたパスセグメントの文字列から、その種類に応じたスコアを計算します。
30
+ *
31
+ * @param s 解析対象のパスセグメント文字列です。
32
+ * @returns セグメントの種類に基づいた数値スコアを返します。
33
+ */
34
+ function getSegmentScore(s: string): number {
35
+ if (s.includes("*")) {
36
+ return SCORE.WILDCARD;
37
+ }
38
+
39
+ if (s.startsWith(":")) {
40
+ if (s.endsWith("?")) {
41
+ return SCORE.OPTIONAL_PARAM;
42
+ } else {
43
+ return SCORE.PARAMETER;
44
+ }
45
+ }
46
+
47
+ return SCORE.STATIC;
48
+ }
49
+
50
+ /**
51
+ * 2 つのパスを比較し、ルーティングの優先順位に基づいたソート順を決定します。
52
+ *
53
+ * より具体的で深いパスが前方に配置されるように比較を行います。
54
+ *
55
+ * @param pathA 比較対象のパス A です。
56
+ * @param pathB 比較対象のパス B です。
57
+ * @returns 比較結果を示す数値です(負数は pathA が優先、正数は pathB が優先)。
58
+ */
59
+ export default function compareRoutePaths(pathA: string, pathB: string): number {
60
+ const partsA = pathA.split("/").filter(Boolean);
61
+ const partsB = pathB.split("/").filter(Boolean);
62
+ const length = Math.max(partsA.length, partsB.length);
63
+
64
+ for (let i = 0; i < length; i++) {
65
+ const a = partsA[i];
66
+ // パス A のセグメントが先に終了した場合は、パス B を優先します。
67
+ if (a === undefined) {
68
+ return 1;
69
+ }
70
+
71
+ const b = partsB[i];
72
+ // パス B のセグメントが先に終了した場合は、パス A を優先します。
73
+ if (b === undefined) {
74
+ return -1;
75
+ }
76
+
77
+ const scoreA = getSegmentScore(a);
78
+ const scoreB = getSegmentScore(b);
79
+ // スコアが異なる場合は、スコアが高い方を優先します。
80
+ if (scoreA !== scoreB) {
81
+ return scoreB - scoreA;
82
+ }
83
+ // スコアが同じ場合は、文字列の辞書順で比較します。
84
+ if (a !== b) {
85
+ return a.localeCompare(b);
86
+ }
87
+ }
88
+
89
+ return 0;
90
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReadonlyFormData } from "./readonly-form-data.types.js";
2
+
3
+ /**
4
+ * ReadonlyFormData の内容から、一時的な HTMLFormElement を生成します。
5
+ *
6
+ * @param formData フォーム要素に変換する元のフォームデータです。
7
+ * @returns 生成された HTMLFormElement を返します。
8
+ */
9
+ export default function createHtmlFormElementFormFormData(
10
+ formData: ReadonlyFormData,
11
+ ): HTMLFormElement {
12
+ const form = document.createElement("form");
13
+ form.style.display = "none";
14
+ formData.forEach((value, name) => {
15
+ const input = document.createElement("input");
16
+ if (value instanceof File) {
17
+ input.type = "file";
18
+ input.name = name;
19
+ const dataTransfer = new DataTransfer();
20
+ dataTransfer.items.add(value);
21
+ input.files = dataTransfer.files;
22
+ } else {
23
+ input.type = "hidden";
24
+ input.name = name;
25
+ input.value = value;
26
+ }
27
+
28
+ form.appendChild(input);
29
+ });
30
+
31
+ return form;
32
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 連続するスラッシュ(/)にマッチする正規表現です。
3
+ */
4
+ const MULTI_SLASH = /\/\/+/gu;
5
+
6
+ /**
7
+ * 与えられたパス名を URL エンコードし、正規化された形式で返します。
8
+ *
9
+ * エンコードの必要がある部分のみをエンコードします。
10
+ *
11
+ * @param pathname エンコード対象となるパス名の文字列です。
12
+ * @returns 正規化およびエンコードされたパス名の文字列です。
13
+ */
14
+ export default function encodePathname(pathname: string): string {
15
+ const u = new URL("x://y" + ("/" + pathname).replace(MULTI_SLASH, "/"));
16
+ return u.pathname;
17
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * エラーオブジェクトのようなオブジェクトかどうか判定します。
3
+ *
4
+ * @param e 判定する値です。
5
+ * @returns 判定結果です。
6
+ */
7
+ function isError(e: unknown): e is Error {
8
+ return e instanceof Error || (
9
+ e !== null
10
+ && typeof e === "object"
11
+ && ("name" in e && typeof e.name === "string")
12
+ && ("message" in e && typeof e.message === "string")
13
+ );
14
+ }
15
+
16
+ export default globalThis.Error.isError || isError;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 与えられた値が `PromiseLike` であるかどうかを判定します。
3
+ *
4
+ * @param value 判定対象の値です。
5
+ * @returns 値が `then` メソッドを持つオブジェクトである場合は `true`、それ以外は `false` を返します。
6
+ */
7
+ export default function isPromiseLike<T = unknown>(value: unknown): value is PromiseLike<T> {
8
+ return (
9
+ value !== null
10
+ && typeof value === "object"
11
+ // @ts-expect-error
12
+ && typeof value["then"] === "function"
13
+ );
14
+ }
@@ -0,0 +1,39 @@
1
+ import type { PathParams, Route } from "./route.types.js";
2
+
3
+ /**
4
+ * パスマッチングの結果を表す型定義です。
5
+ */
6
+ export type MatchPathResult = {
7
+ /**
8
+ * パスから抽出されたパラメーターのキーと値のペアです。
9
+ */
10
+ params: PathParams;
11
+ };
12
+
13
+ /**
14
+ * 指定されたルートの正規表現パターンを使用して、パス名がマッチするかどうかを判定します。
15
+ *
16
+ * @param route 判定対象となる前処理済みのルートオブジェクトです。
17
+ * @param pathname マッチングを行う対象のパス名(URL のパス部分)です。
18
+ * @returns マッチした場合はパラメーターを含むオブジェクトを返し、マッチしない場合は `null` を返します。
19
+ */
20
+ export default function matchPath(
21
+ route: Pick<Route, "paramKeys" | "pathPattern">,
22
+ pathname: string,
23
+ ): MatchPathResult | null {
24
+ const matches = route.pathPattern.exec(pathname);
25
+ if (!matches) {
26
+ return null;
27
+ }
28
+
29
+ const params: Record<string, string> = {};
30
+ for (let i = 0, value: string | undefined; i < route.paramKeys.length; i++) {
31
+ value = matches[i + 1];
32
+ if (value !== undefined) {
33
+ // route.paramKeys のインデックスと matches のキャプチャーグループを対応させます。
34
+ params[route.paramKeys[i]!] = value;
35
+ }
36
+ }
37
+
38
+ return { params };
39
+ }