@octanejs/remix-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/package.json +53 -0
  4. package/src/dom.ts +16 -0
  5. package/src/index.ts +267 -0
  6. package/src/internal.ts +37 -0
  7. package/src/lib/Await.tsrx +145 -0
  8. package/src/lib/Await.tsrx.d.ts +6 -0
  9. package/src/lib/DefaultErrorComponent.tsrx +47 -0
  10. package/src/lib/DefaultErrorComponent.tsrx.d.ts +2 -0
  11. package/src/lib/RenderErrorBoundary.tsrx +117 -0
  12. package/src/lib/RenderErrorBoundary.tsrx.d.ts +14 -0
  13. package/src/lib/actions.ts +90 -0
  14. package/src/lib/components/MemoryRouter.tsrx +42 -0
  15. package/src/lib/components/MemoryRouter.tsrx.d.ts +10 -0
  16. package/src/lib/components/Navigate.ts +60 -0
  17. package/src/lib/components/Router.tsrx +88 -0
  18. package/src/lib/components/Router.tsrx.d.ts +13 -0
  19. package/src/lib/components/RouterProvider.tsrx +320 -0
  20. package/src/lib/components/RouterProvider.tsrx.d.ts +21 -0
  21. package/src/lib/components/Routes.tsrx +53 -0
  22. package/src/lib/components/Routes.tsrx.d.ts +7 -0
  23. package/src/lib/components/routes-collector.ts +317 -0
  24. package/src/lib/components/utils.ts +171 -0
  25. package/src/lib/components/with-props.ts +72 -0
  26. package/src/lib/context.ts +129 -0
  27. package/src/lib/dom/Form.tsrx +76 -0
  28. package/src/lib/dom/Form.tsrx.d.ts +22 -0
  29. package/src/lib/dom/Link.tsrx +91 -0
  30. package/src/lib/dom/Link.tsrx.d.ts +22 -0
  31. package/src/lib/dom/NavLink.tsrx +118 -0
  32. package/src/lib/dom/NavLink.tsrx.d.ts +33 -0
  33. package/src/lib/dom/dom.ts +344 -0
  34. package/src/lib/dom/hooks.ts +93 -0
  35. package/src/lib/dom/lib.ts +822 -0
  36. package/src/lib/dom/routers.tsrx +104 -0
  37. package/src/lib/dom/routers.tsrx.d.ts +23 -0
  38. package/src/lib/dom/server.tsrx +350 -0
  39. package/src/lib/dom/server.tsrx.d.ts +25 -0
  40. package/src/lib/errors.ts +84 -0
  41. package/src/lib/framework-stubs.ts +103 -0
  42. package/src/lib/hooks.ts +1797 -0
  43. package/src/lib/href.ts +71 -0
  44. package/src/lib/react-types.ts +10 -0
  45. package/src/lib/router/history.ts +763 -0
  46. package/src/lib/router/instrumentation.ts +496 -0
  47. package/src/lib/router/links.ts +192 -0
  48. package/src/lib/router/router.ts +7165 -0
  49. package/src/lib/router/server-runtime-types.ts +12 -0
  50. package/src/lib/router/url.ts +8 -0
  51. package/src/lib/router/utils.ts +2179 -0
  52. package/src/lib/server-runtime/cookies.ts +240 -0
  53. package/src/lib/server-runtime/crypto.ts +55 -0
  54. package/src/lib/server-runtime/mode.ts +16 -0
  55. package/src/lib/server-runtime/sessions/cookieStorage.ts +54 -0
  56. package/src/lib/server-runtime/sessions/memoryStorage.ts +59 -0
  57. package/src/lib/server-runtime/sessions.ts +291 -0
  58. package/src/lib/server-runtime/warnings.ts +10 -0
  59. package/src/lib/types/future.ts +16 -0
  60. package/src/lib/types/params.ts +8 -0
  61. package/src/lib/types/register.ts +39 -0
  62. package/src/lib/types/route-module.ts +17 -0
  63. package/src/lib/types/utils.ts +37 -0
@@ -0,0 +1,344 @@
1
+ // Vendored from react-router@7.18.1 packages/react-router/lib/dom/dom.ts — unmodified.
2
+ // Re-vendor with `node scripts/vendor-remix-router.mjs`; never hand-edit.
3
+ import { warning } from '../router/history';
4
+ import type { RelativeRoutingType } from '../router/router';
5
+ import type { FormEncType, HTMLFormMethod } from '../router/utils';
6
+ import { stripBasename } from '../router/utils';
7
+
8
+ export const defaultMethod: HTMLFormMethod = 'get';
9
+ const defaultEncType: FormEncType = 'application/x-www-form-urlencoded';
10
+
11
+ export function isHtmlElement(object: any): object is HTMLElement {
12
+ return typeof HTMLElement !== 'undefined' && object instanceof HTMLElement;
13
+ }
14
+
15
+ export function isButtonElement(object: any): object is HTMLButtonElement {
16
+ return isHtmlElement(object) && object.tagName.toLowerCase() === 'button';
17
+ }
18
+
19
+ export function isFormElement(object: any): object is HTMLFormElement {
20
+ return isHtmlElement(object) && object.tagName.toLowerCase() === 'form';
21
+ }
22
+
23
+ export function isInputElement(object: any): object is HTMLInputElement {
24
+ return isHtmlElement(object) && object.tagName.toLowerCase() === 'input';
25
+ }
26
+
27
+ type LimitedMouseEvent = Pick<MouseEvent, 'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey'>;
28
+
29
+ function isModifiedEvent(event: LimitedMouseEvent) {
30
+ return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
31
+ }
32
+
33
+ export function shouldProcessLinkClick(event: LimitedMouseEvent, target?: string) {
34
+ return (
35
+ event.button === 0 && // Ignore everything but left clicks
36
+ (!target || target === '_self') && // Let browser handle "target=_blank" etc.
37
+ !isModifiedEvent(event) // Ignore clicks with modifier keys
38
+ );
39
+ }
40
+
41
+ export type ParamKeyValuePair = [string, string];
42
+
43
+ export type URLSearchParamsInit =
44
+ | string
45
+ | ParamKeyValuePair[]
46
+ | Record<string, string | string[]>
47
+ | URLSearchParams;
48
+
49
+ /**
50
+ Creates a URLSearchParams object using the given initializer.
51
+
52
+ This is identical to `new URLSearchParams(init)` except it also
53
+ supports arrays as values in the object form of the initializer
54
+ instead of just strings. This is convenient when you need multiple
55
+ values for a given key, but don't want to use an array initializer.
56
+
57
+ For example, instead of:
58
+
59
+ ```tsx
60
+ let searchParams = new URLSearchParams([
61
+ ['sort', 'name'],
62
+ ['sort', 'price']
63
+ ]);
64
+ ```
65
+ you can do:
66
+
67
+ ```
68
+ let searchParams = createSearchParams({
69
+ sort: ['name', 'price']
70
+ });
71
+ ```
72
+
73
+ @category Utils
74
+ */
75
+ export function createSearchParams(init: URLSearchParamsInit = ''): URLSearchParams {
76
+ return new URLSearchParams(
77
+ typeof init === 'string' || Array.isArray(init) || init instanceof URLSearchParams
78
+ ? init
79
+ : Object.keys(init).reduce((memo, key) => {
80
+ let value = init[key];
81
+ return memo.concat(Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]);
82
+ }, [] as ParamKeyValuePair[]),
83
+ );
84
+ }
85
+
86
+ export function getSearchParamsForLocation(
87
+ locationSearch: string,
88
+ defaultSearchParams: URLSearchParams | null,
89
+ ) {
90
+ let searchParams = createSearchParams(locationSearch);
91
+
92
+ if (defaultSearchParams) {
93
+ // Use `defaultSearchParams.forEach(...)` here instead of iterating of
94
+ // `defaultSearchParams.keys()` to work-around a bug in Firefox related to
95
+ // web extensions. Relevant Bugzilla tickets:
96
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1414602
97
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1023984
98
+ defaultSearchParams.forEach((_, key) => {
99
+ if (!searchParams.has(key)) {
100
+ defaultSearchParams.getAll(key).forEach((value) => {
101
+ searchParams.append(key, value);
102
+ });
103
+ }
104
+ });
105
+ }
106
+
107
+ return searchParams;
108
+ }
109
+
110
+ // Thanks https://github.com/sindresorhus/type-fest!
111
+ type JsonObject = { [Key in string]: JsonValue } & {
112
+ [Key in string]?: JsonValue | undefined;
113
+ };
114
+ type JsonArray = JsonValue[] | readonly JsonValue[];
115
+ type JsonPrimitive = string | number | boolean | null;
116
+ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
117
+
118
+ export type SubmitTarget =
119
+ | HTMLFormElement
120
+ | HTMLButtonElement
121
+ | HTMLInputElement
122
+ | FormData
123
+ | URLSearchParams
124
+ | JsonValue
125
+ | null;
126
+
127
+ // One-time check for submitter support
128
+ let _formDataSupportsSubmitter: boolean | null = null;
129
+
130
+ function isFormDataSubmitterSupported() {
131
+ if (_formDataSupportsSubmitter === null) {
132
+ try {
133
+ new FormData(
134
+ document.createElement('form'),
135
+ // @ts-expect-error if FormData supports the submitter parameter, this will throw
136
+ 0,
137
+ );
138
+ _formDataSupportsSubmitter = false;
139
+ } catch (
140
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
+ e
142
+ ) {
143
+ _formDataSupportsSubmitter = true;
144
+ }
145
+ }
146
+ return _formDataSupportsSubmitter;
147
+ }
148
+
149
+ /**
150
+ * Submit options shared by both navigations and fetchers
151
+ */
152
+ interface SharedSubmitOptions {
153
+ /**
154
+ * The HTTP method used to submit the form. Overrides `<form method>`.
155
+ * Defaults to "GET".
156
+ */
157
+ method?: HTMLFormMethod;
158
+
159
+ /**
160
+ * The action URL path used to submit the form. Overrides `<form action>`.
161
+ * Defaults to the path of the current route.
162
+ */
163
+ action?: string;
164
+
165
+ /**
166
+ * The encoding used to submit the form. Overrides `<form encType>`.
167
+ * Defaults to "application/x-www-form-urlencoded".
168
+ */
169
+ encType?: FormEncType;
170
+
171
+ /**
172
+ * Determines whether the form action is relative to the route hierarchy or
173
+ * the pathname. Use this if you want to opt out of navigating the route
174
+ * hierarchy and want to instead route based on /-delimited URL segments
175
+ */
176
+ relative?: RelativeRoutingType;
177
+
178
+ /**
179
+ * In browser-based environments, prevent resetting scroll after this
180
+ * navigation when using the <ScrollRestoration> component
181
+ */
182
+ preventScrollReset?: boolean;
183
+
184
+ /**
185
+ * Enable flushSync for this submission's state updates
186
+ */
187
+ flushSync?: boolean;
188
+
189
+ /**
190
+ * Specify the default revalidation behavior after this submission
191
+ *
192
+ * If no `shouldRevalidate` functions are present on the active routes, then this
193
+ * value will be used directly. Otherwise it will be passed into `shouldRevalidate`
194
+ * so the route can make the final determination on revalidation. This can be
195
+ * useful when updating search params and you don't want to trigger a revalidation.
196
+ *
197
+ * By default (when not specified), loaders will revalidate according to the routers
198
+ * standard revalidation behavior.
199
+ */
200
+ defaultShouldRevalidate?: boolean;
201
+ }
202
+
203
+ /**
204
+ * Submit options available to fetchers
205
+ */
206
+ export interface FetcherSubmitOptions extends SharedSubmitOptions {}
207
+
208
+ /**
209
+ * Submit options available to navigations
210
+ */
211
+ export interface SubmitOptions extends FetcherSubmitOptions {
212
+ /**
213
+ * Set `true` to replace the current entry in the browser's history stack
214
+ * instead of creating a new one (i.e. stay on "the same page"). Defaults
215
+ * to `false`.
216
+ */
217
+ replace?: boolean;
218
+
219
+ /**
220
+ * State object to add to the history stack entry for this navigation
221
+ */
222
+ state?: any;
223
+
224
+ /**
225
+ * Indicate a specific fetcherKey to use when using navigate=false
226
+ */
227
+ fetcherKey?: string;
228
+
229
+ /**
230
+ * navigate=false will use a fetcher instead of a navigation
231
+ */
232
+ navigate?: boolean;
233
+
234
+ /**
235
+ * Enable view transitions on this submission navigation
236
+ */
237
+ viewTransition?: boolean;
238
+ }
239
+
240
+ const supportedFormEncTypes: Set<FormEncType> = new Set([
241
+ 'application/x-www-form-urlencoded',
242
+ 'multipart/form-data',
243
+ 'text/plain',
244
+ ]);
245
+
246
+ function getFormEncType(encType: string | null) {
247
+ if (encType != null && !supportedFormEncTypes.has(encType as FormEncType)) {
248
+ warning(
249
+ false,
250
+ `"${encType}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` ` +
251
+ `and will default to "${defaultEncType}"`,
252
+ );
253
+
254
+ return null;
255
+ }
256
+ return encType;
257
+ }
258
+
259
+ export function getFormSubmissionInfo(
260
+ target: SubmitTarget,
261
+ basename: string,
262
+ ): {
263
+ action: string | null;
264
+ method: string;
265
+ encType: string;
266
+ formData: FormData | undefined;
267
+ body: any;
268
+ } {
269
+ let method: string;
270
+ let action: string | null;
271
+ let encType: string;
272
+ let formData: FormData | undefined;
273
+ let body: any;
274
+
275
+ if (isFormElement(target)) {
276
+ // When grabbing the action from the element, it will have had the basename
277
+ // prefixed to ensure non-JS scenarios work, so strip it since we'll
278
+ // re-prefix in the router
279
+ let attr = target.getAttribute('action');
280
+ action = attr ? stripBasename(attr, basename) : null;
281
+ method = target.getAttribute('method') || defaultMethod;
282
+ encType = getFormEncType(target.getAttribute('enctype')) || defaultEncType;
283
+
284
+ formData = new FormData(target);
285
+ } else if (
286
+ isButtonElement(target) ||
287
+ (isInputElement(target) && (target.type === 'submit' || target.type === 'image'))
288
+ ) {
289
+ let form = target.form;
290
+
291
+ if (form == null) {
292
+ throw new Error(`Cannot submit a <button> or <input type="submit"> without a <form>`);
293
+ }
294
+
295
+ // <button>/<input type="submit"> may override attributes of <form>
296
+
297
+ // When grabbing the action from the element, it will have had the basename
298
+ // prefixed to ensure non-JS scenarios work, so strip it since we'll
299
+ // re-prefix in the router
300
+ let attr = target.getAttribute('formaction') || form.getAttribute('action');
301
+ action = attr ? stripBasename(attr, basename) : null;
302
+
303
+ method = target.getAttribute('formmethod') || form.getAttribute('method') || defaultMethod;
304
+ encType =
305
+ getFormEncType(target.getAttribute('formenctype')) ||
306
+ getFormEncType(form.getAttribute('enctype')) ||
307
+ defaultEncType;
308
+
309
+ // Build a FormData object populated from a form and submitter
310
+ formData = new FormData(form, target);
311
+
312
+ // If this browser doesn't support the `FormData(el, submitter)` format,
313
+ // then tack on the submitter value at the end. This is a lightweight
314
+ // solution that is not 100% spec compliant. For complete support in older
315
+ // browsers, consider using the `formdata-submitter-polyfill` package
316
+ if (!isFormDataSubmitterSupported()) {
317
+ let { name, type, value } = target;
318
+ if (type === 'image') {
319
+ let prefix = name ? `${name}.` : '';
320
+ formData.append(`${prefix}x`, '0');
321
+ formData.append(`${prefix}y`, '0');
322
+ } else if (name) {
323
+ formData.append(name, value);
324
+ }
325
+ }
326
+ } else if (isHtmlElement(target)) {
327
+ throw new Error(
328
+ `Cannot submit element that is not <form>, <button>, or ` + `<input type="submit|image">`,
329
+ );
330
+ } else {
331
+ method = defaultMethod;
332
+ action = null;
333
+ encType = defaultEncType;
334
+ body = target;
335
+ }
336
+
337
+ // Send body for <Form encType="text/plain" so we encode it into text
338
+ if (formData && encType === 'text/plain') {
339
+ body = formData;
340
+ formData = undefined;
341
+ }
342
+
343
+ return { action, method: method.toLowerCase(), encType, formData, body };
344
+ }
@@ -0,0 +1,93 @@
1
+ // DOM hooks — transcribed from react-router@7.18.1 lib/dom/lib.tsx onto
2
+ // octane. This PR ships useLinkClickHandler (Link's engine); the remaining DOM
3
+ // hooks (useSearchParams, useSubmit, useFetcher, …) land in later phases per
4
+ // docs/remix-router-port-plan.md.
5
+ import { startTransition, useCallback } from 'octane';
6
+ import type { To } from '../router/history';
7
+ import { createPath } from '../router/history';
8
+ import type { RelativeRoutingType } from '../router/router';
9
+ import { shouldProcessLinkClick } from './dom';
10
+ import { useLocation, useNavigate, useResolvedPath } from '../hooks';
11
+ import { splitSlot, subSlot } from '../../internal';
12
+
13
+ /**
14
+ * Handles the click behavior for router `<Link>` components. This is useful if
15
+ * you need to create custom `<Link>` components with the same click behavior we
16
+ * use in our exported `<Link>`.
17
+ */
18
+ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
19
+ to: To,
20
+ ...rest: unknown[]
21
+ ): (event: MouseEvent & { currentTarget: E }) => void {
22
+ const [user, slot] = splitSlot(rest as any[]);
23
+ const {
24
+ target,
25
+ replace: replaceProp,
26
+ mask,
27
+ state,
28
+ preventScrollReset,
29
+ relative,
30
+ viewTransition,
31
+ defaultShouldRevalidate,
32
+ useTransitions,
33
+ } = (user[0] ?? {}) as {
34
+ target?: string;
35
+ replace?: boolean;
36
+ mask?: To;
37
+ state?: any;
38
+ preventScrollReset?: boolean;
39
+ relative?: RelativeRoutingType;
40
+ viewTransition?: boolean;
41
+ defaultShouldRevalidate?: boolean;
42
+ useTransitions?: boolean;
43
+ };
44
+ const navigate = useNavigate(subSlot(slot, 'ulch:nav')) as (to: To, opts?: any) => void;
45
+ const location = useLocation();
46
+ const path = useResolvedPath(to, { relative }, subSlot(slot, 'ulch:path')) as any;
47
+
48
+ return useCallback(
49
+ (event: MouseEvent) => {
50
+ if (shouldProcessLinkClick(event as any, target)) {
51
+ event.preventDefault();
52
+
53
+ // If the URL hasn't changed, a regular <a> will do a replace instead of
54
+ // a push, so do the same here unless the replace prop is explicitly set
55
+ const replace =
56
+ replaceProp !== undefined ? replaceProp : createPath(location) === createPath(path);
57
+
58
+ const doNavigate = () =>
59
+ navigate(to, {
60
+ replace,
61
+ mask,
62
+ state,
63
+ preventScrollReset,
64
+ relative,
65
+ viewTransition,
66
+ defaultShouldRevalidate,
67
+ });
68
+
69
+ if (useTransitions) {
70
+ startTransition(() => doNavigate());
71
+ } else {
72
+ doNavigate();
73
+ }
74
+ }
75
+ },
76
+ [
77
+ location,
78
+ navigate,
79
+ path,
80
+ replaceProp,
81
+ mask,
82
+ state,
83
+ target,
84
+ to,
85
+ preventScrollReset,
86
+ relative,
87
+ viewTransition,
88
+ defaultShouldRevalidate,
89
+ useTransitions,
90
+ ],
91
+ subSlot(slot, 'ulch:cb'),
92
+ );
93
+ }