@solidjs/router 0.17.0-next.6 → 1.0.0-next.8
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.md +526 -716
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +15 -0
- package/dist/data/action.js +127 -12
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +23 -22
- package/dist/data/flash.d.ts +1 -5
- package/dist/data/flash.js +10 -18
- package/dist/data/flashCookie.d.ts +7 -0
- package/dist/data/flashCookie.js +20 -0
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1736 -1157
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +10 -21
- package/dist/routers/components.jsx +29 -51
- package/dist/routers/factory.d.ts +45 -0
- package/dist/routers/factory.jsx +143 -0
- package/dist/routers/history.d.ts +24 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +4 -11
- package/dist/routers/index.js +2 -6
- package/dist/routing.d.ts +79 -52
- package/dist/routing.js +296 -127
- package/dist/server.d.ts +25 -15
- package/dist/server.js +43 -7
- package/dist/types.d.ts +74 -5
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +6 -6
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -46
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -9
- package/dist/routers/Router.js +0 -45
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -40
package/dist/claims.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RouterContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The compiler claims every `a[href]` (and `form[action]`, which this handler
|
|
4
|
+
* ignores) at creation, and the runtime re-claims on `href` writes. This
|
|
5
|
+
* consumer gives each router-managed anchor the link-state vocabulary without
|
|
6
|
+
* a wrapper component:
|
|
7
|
+
*
|
|
8
|
+
* - `aria-current="page"` — the location matches the link exactly
|
|
9
|
+
* - `data-active` — exact or prefix match
|
|
10
|
+
* - `data-pending` — the link is the target of an in-flight navigation
|
|
11
|
+
*
|
|
12
|
+
* Elements are claimed at creation, so late mounts (`<Show>`, `<For>`,
|
|
13
|
+
* portals) are correct immediately. One render effect (owned by the router)
|
|
14
|
+
* subscribes to the location and sweeps a registry of claimed anchors —
|
|
15
|
+
* anchors themselves carry no reactive machinery, just a registry entry
|
|
16
|
+
* removed by their creating owner's cleanup. State is applied once at claim
|
|
17
|
+
* so it is correct before the next navigation; re-claims (an `href` write)
|
|
18
|
+
* are the same one-shot untracked refresh, reading the element's current
|
|
19
|
+
* `href` from the DOM.
|
|
20
|
+
*/
|
|
21
|
+
export declare function setupLinkClaims(router: RouterContext, explicitLinks?: boolean): void;
|
package/dist/claims.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { registerElementClaim } from "@solidjs/web";
|
|
2
|
+
import { createRenderEffect, getOwner, onCleanup, untrack } from "solid-js";
|
|
3
|
+
import { comparablePath } from "./utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* The compiler claims every `a[href]` (and `form[action]`, which this handler
|
|
6
|
+
* ignores) at creation, and the runtime re-claims on `href` writes. This
|
|
7
|
+
* consumer gives each router-managed anchor the link-state vocabulary without
|
|
8
|
+
* a wrapper component:
|
|
9
|
+
*
|
|
10
|
+
* - `aria-current="page"` — the location matches the link exactly
|
|
11
|
+
* - `data-active` — exact or prefix match
|
|
12
|
+
* - `data-pending` — the link is the target of an in-flight navigation
|
|
13
|
+
*
|
|
14
|
+
* Elements are claimed at creation, so late mounts (`<Show>`, `<For>`,
|
|
15
|
+
* portals) are correct immediately. One render effect (owned by the router)
|
|
16
|
+
* subscribes to the location and sweeps a registry of claimed anchors —
|
|
17
|
+
* anchors themselves carry no reactive machinery, just a registry entry
|
|
18
|
+
* removed by their creating owner's cleanup. State is applied once at claim
|
|
19
|
+
* so it is correct before the next navigation; re-claims (an `href` write)
|
|
20
|
+
* are the same one-shot untracked refresh, reading the element's current
|
|
21
|
+
* `href` from the DOM.
|
|
22
|
+
*/
|
|
23
|
+
export function setupLinkClaims(router, explicitLinks) {
|
|
24
|
+
const basePath = router.base.path();
|
|
25
|
+
// per-element record; `current` remembers whether we set `aria-current`,
|
|
26
|
+
// so user-authored values (steppers, breadcrumbs) are never stripped
|
|
27
|
+
const claimed = new WeakMap();
|
|
28
|
+
const registry = new Set();
|
|
29
|
+
function isSvg(el) {
|
|
30
|
+
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
31
|
+
}
|
|
32
|
+
/** The comparable pathname when the router manages this anchor, else `undefined`. */
|
|
33
|
+
function managedPath(a) {
|
|
34
|
+
if (explicitLinks && !a.hasAttribute("link"))
|
|
35
|
+
return;
|
|
36
|
+
const svg = isSvg(a);
|
|
37
|
+
// claims fire at creation while the element is still in the template's
|
|
38
|
+
// inert fragment, where the `href` property is not resolved — resolve the
|
|
39
|
+
// raw attribute against the live document instead
|
|
40
|
+
const href = svg ? a.href.baseVal : a.getAttribute("href");
|
|
41
|
+
const target = svg ? a.target.baseVal : a.target;
|
|
42
|
+
if (target || !href)
|
|
43
|
+
return;
|
|
44
|
+
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
45
|
+
if (a.hasAttribute("download") || rel.includes("external"))
|
|
46
|
+
return;
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(href, document.baseURI);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (url.origin !== window.location.origin ||
|
|
55
|
+
(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
|
|
56
|
+
return;
|
|
57
|
+
return comparablePath(url.pathname);
|
|
58
|
+
}
|
|
59
|
+
function linkState(a) {
|
|
60
|
+
// read reactive sources unconditionally so the owning effect stays
|
|
61
|
+
// subscribed even while the anchor is not router-managed
|
|
62
|
+
const loc = decodeURI(comparablePath(router.location.pathname));
|
|
63
|
+
const routing = router.isRouting();
|
|
64
|
+
const path = managedPath(a);
|
|
65
|
+
// the root path is a prefix of everything, so it only matches exactly —
|
|
66
|
+
// there is no per-anchor `end` opt-out like useLinkState has
|
|
67
|
+
const matches = (target) => path !== undefined && (target === path || (path !== "" && target.startsWith(path + "/")));
|
|
68
|
+
// effects observe the committed location during a transition, so the
|
|
69
|
+
// in-flight target comes from pendingTarget — readable here because the
|
|
70
|
+
// isRouting write flushes after the target is assigned
|
|
71
|
+
const pending = routing && !!router.pendingTarget && matches(decodeURI(comparablePath(router.pendingTarget.value)));
|
|
72
|
+
return { active: matches(loc), pending, exact: path !== undefined && loc === path };
|
|
73
|
+
}
|
|
74
|
+
function apply(a, rec, { active, pending, exact }) {
|
|
75
|
+
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
76
|
+
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
77
|
+
if (exact !== rec.current) {
|
|
78
|
+
exact ? a.setAttribute("aria-current", "page") : a.removeAttribute("aria-current");
|
|
79
|
+
rec.current = exact;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
83
|
+
// The one subscription for every anchor: compute tracks the sources
|
|
84
|
+
// linkState derives from (the in-flight pendingTarget is readable in the
|
|
85
|
+
// effect phase because the isRouting write flushes after the target is
|
|
86
|
+
// assigned), the effect phase sweeps the registry untracked.
|
|
87
|
+
//
|
|
88
|
+
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
89
|
+
// This setup is client-only, so an id-consuming node here has no server
|
|
90
|
+
// counterpart and every subsequent hydration id would shift by one child
|
|
91
|
+
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
92
|
+
// unclaimed. (The option is honored by the runtime but missing from the
|
|
93
|
+
// published EffectOptions type, hence the cast.)
|
|
94
|
+
createRenderEffect(() => (router.location.pathname, router.isRouting()), () => registry.forEach(a => refresh(a, claimed.get(a))), { transparent: true });
|
|
95
|
+
onCleanup(registerElementClaim(node => {
|
|
96
|
+
if (node.nodeName.toUpperCase() !== "A")
|
|
97
|
+
return;
|
|
98
|
+
const a = node;
|
|
99
|
+
// re-claim (href changed): the claiming write runs inside another
|
|
100
|
+
// effect, so refresh without leaking subscriptions into it
|
|
101
|
+
const existing = claimed.get(a);
|
|
102
|
+
if (existing)
|
|
103
|
+
return refresh(a, existing);
|
|
104
|
+
const rec = { current: false };
|
|
105
|
+
claimed.set(a, rec);
|
|
106
|
+
// claims fire during component setup, so an owner is present in
|
|
107
|
+
// practice to bound the registry entry's lifetime; without one, state
|
|
108
|
+
// is still applied once at creation
|
|
109
|
+
if (getOwner()) {
|
|
110
|
+
registry.add(a);
|
|
111
|
+
onCleanup(() => registry.delete(a));
|
|
112
|
+
}
|
|
113
|
+
refresh(a, rec);
|
|
114
|
+
}));
|
|
115
|
+
}
|
package/dist/data/action.d.ts
CHANGED
|
@@ -13,6 +13,21 @@ type ActionFactory = {
|
|
|
13
13
|
}): Action<T, U>;
|
|
14
14
|
};
|
|
15
15
|
export declare const actions: Map<string, Action<any, any, any>>;
|
|
16
|
+
/**
|
|
17
|
+
* The document-delegation submit handler for router actions. Lives here —
|
|
18
|
+
* not in events.ts — so the router's event wiring holds no static reference
|
|
19
|
+
* to the action module; `installRouterIntegrations` slots it in when the
|
|
20
|
+
* first action is created on the client.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleFormAction(evt: SubmitEvent, router: RouterContext, actionBase: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
25
|
+
* handler was ever installed — no action module in the client graph at all —
|
|
26
|
+
* the router intercepts posts to server-action urls synchronously and loads
|
|
27
|
+
* this module to run them. The FormData was captured at submit time; only
|
|
28
|
+
* the enctype conversion and the generic invocation happen here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function submitServerForm(router: RouterContext, url: string, form: HTMLFormElement, data: FormData): void;
|
|
16
31
|
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[];
|
|
17
32
|
export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
|
|
18
33
|
export declare const action: ActionFactory;
|
package/dist/data/action.js
CHANGED
|
@@ -1,13 +1,119 @@
|
|
|
1
1
|
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
2
|
import { isResponseEnvelope, isServer } from "@solidjs/web";
|
|
3
|
-
import { decodeResponse, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
|
-
import { useRouter } from "../routing.js";
|
|
3
|
+
import { createServerReference, decodeResponse, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
|
+
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
5
|
+
import { setRouterFormHandler } from "./events.js";
|
|
6
|
+
import { decodeFlashCookie } from "./flash.js";
|
|
5
7
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
6
8
|
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
7
9
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
8
10
|
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
9
11
|
const invokeSymbol = Symbol("routerActionInvoke");
|
|
12
|
+
// Forms submitted through delegation are marked `aria-busy` while their
|
|
13
|
+
// action is in flight — the form half of the attribute vocabulary links get
|
|
14
|
+
// (`data-active`/`data-pending`). Style with `form[aria-busy] button { ... }`.
|
|
15
|
+
// A counter (not a boolean) keeps the attribute through overlapping
|
|
16
|
+
// submissions from the same form.
|
|
17
|
+
const busyForms = /* #__PURE__ */ new WeakMap();
|
|
18
|
+
function setFormBusy(form, delta) {
|
|
19
|
+
const count = (busyForms.get(form) || 0) + delta;
|
|
20
|
+
busyForms.set(form, count);
|
|
21
|
+
count > 0 ? form.setAttribute("aria-busy", "true") : form.removeAttribute("aria-busy");
|
|
22
|
+
}
|
|
10
23
|
export const actions = /* #__PURE__ */ new Map();
|
|
24
|
+
/**
|
|
25
|
+
* The document-delegation submit handler for router actions. Lives here —
|
|
26
|
+
* not in events.ts — so the router's event wiring holds no static reference
|
|
27
|
+
* to the action module; `installRouterIntegrations` slots it in when the
|
|
28
|
+
* first action is created on the client.
|
|
29
|
+
*/
|
|
30
|
+
export function handleFormAction(evt, router, actionBase) {
|
|
31
|
+
if (evt.defaultPrevented)
|
|
32
|
+
return;
|
|
33
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
34
|
+
? evt.submitter.getAttribute("formaction")
|
|
35
|
+
: evt.target.getAttribute("action");
|
|
36
|
+
if (!actionRef)
|
|
37
|
+
return;
|
|
38
|
+
const serverAction = !actionRef.startsWith("https://action/");
|
|
39
|
+
if (serverAction) {
|
|
40
|
+
// normalize server actions
|
|
41
|
+
const url = new URL(actionRef, mockBase);
|
|
42
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
43
|
+
if (!actionRef.startsWith(actionBase))
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (evt.target.method.toUpperCase() !== "POST")
|
|
47
|
+
throw new Error("Only POST forms are supported for Actions");
|
|
48
|
+
// A registry miss on a server-action url is a direct bind whose module
|
|
49
|
+
// never loaded client-side (server components): the url is self-describing
|
|
50
|
+
// (`?id`, bound `?args`), so a generic invocation is synthesized from it —
|
|
51
|
+
// delegation alone is sufficient, the no-JS path stays a no-JS fallback.
|
|
52
|
+
// Client-only actions (`https://action/`) are their module's JS by
|
|
53
|
+
// definition, so a miss there falls through to native submission.
|
|
54
|
+
const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
55
|
+
if (handler) {
|
|
56
|
+
evt.preventDefault();
|
|
57
|
+
const data = new FormData(evt.target, evt.submitter);
|
|
58
|
+
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
59
|
+
? data
|
|
60
|
+
: new URLSearchParams(data));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Synthesizes a router action for a server-rendered action url. The url
|
|
65
|
+
* carries everything an invocation needs — the function id and any bound
|
|
66
|
+
* `.with()` arguments (plain JSON in `?args`, which the server prepends for
|
|
67
|
+
* natural-encoding bodies exactly as it does for no-JS posts) — so the
|
|
68
|
+
* FormData is posted to it verbatim through the server-function transport:
|
|
69
|
+
* submissions, `aria-busy`, redirects, revalidation, and single-flight all
|
|
70
|
+
* flow through the normal action machinery. Registered under the url, so
|
|
71
|
+
* repeat submits reuse it (and a later real registration overrides it).
|
|
72
|
+
*/
|
|
73
|
+
function createServerFormAction(url) {
|
|
74
|
+
const id = new URL(url, mockBase).searchParams.get("id");
|
|
75
|
+
if (!id)
|
|
76
|
+
return undefined;
|
|
77
|
+
// typecheck resolves the server half of the dual module; this path only
|
|
78
|
+
// runs in the browser, where the client transport's signature applies
|
|
79
|
+
const stub = createServerReference(id, undefined, url);
|
|
80
|
+
const caller = Object.assign((form) => stub(form), { url });
|
|
81
|
+
return actionImpl(caller);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
85
|
+
* handler was ever installed — no action module in the client graph at all —
|
|
86
|
+
* the router intercepts posts to server-action urls synchronously and loads
|
|
87
|
+
* this module to run them. The FormData was captured at submit time; only
|
|
88
|
+
* the enctype conversion and the generic invocation happen here.
|
|
89
|
+
*/
|
|
90
|
+
export function submitServerForm(router, url, form, data) {
|
|
91
|
+
const handler = actions.get(url) || createServerFormAction(url);
|
|
92
|
+
// no `?id` — not the server function convention; nothing can run it,
|
|
93
|
+
// resubmit natively (submit() bypasses the delegated handler)
|
|
94
|
+
if (!handler)
|
|
95
|
+
return form.submit();
|
|
96
|
+
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
97
|
+
}
|
|
98
|
+
// Wires the action layer into the router's slots exactly once, triggered by
|
|
99
|
+
// the first action creation. Not an import side effect — with
|
|
100
|
+
// `sideEffects: false`, module evaluation only happens when action() is
|
|
101
|
+
// actually used, which is precisely when the wiring is wanted: no action in
|
|
102
|
+
// the graph means no form interception, no single-flight subscription (the
|
|
103
|
+
// server is never asked to collect), and no flash cookies to decode. On the
|
|
104
|
+
// server, actions are created at module scope, so the flash decoder is
|
|
105
|
+
// always installed before useSubmission can read the submissions signal.
|
|
106
|
+
let integrationsInstalled = false;
|
|
107
|
+
function installRouterIntegrations() {
|
|
108
|
+
if (integrationsInstalled)
|
|
109
|
+
return;
|
|
110
|
+
integrationsInstalled = true;
|
|
111
|
+
provideFlashDecoder(decodeFlashCookie);
|
|
112
|
+
if (!isServer) {
|
|
113
|
+
setRouterFormHandler(handleFormAction);
|
|
114
|
+
provideFlightConsumer(setupFlightDataConsumer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
11
117
|
export function useSubmissions(fn, filter) {
|
|
12
118
|
const router = useRouter();
|
|
13
119
|
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
@@ -48,16 +154,24 @@ function actionImpl(fn, options = {}) {
|
|
|
48
154
|
return { error: true, value: error };
|
|
49
155
|
}
|
|
50
156
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
157
|
+
form && setFormBusy(form, 1);
|
|
158
|
+
let settled;
|
|
159
|
+
let response;
|
|
160
|
+
try {
|
|
161
|
+
settled = await settleActionResult(run({
|
|
162
|
+
call: runMutation,
|
|
163
|
+
optimistic: submitHooks.size
|
|
164
|
+
? () => {
|
|
165
|
+
for (const hook of submitHooks.values())
|
|
166
|
+
hook(...variables);
|
|
167
|
+
}
|
|
168
|
+
: undefined
|
|
169
|
+
}));
|
|
170
|
+
response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
form && setFormBusy(form, -1);
|
|
174
|
+
}
|
|
61
175
|
if (!response)
|
|
62
176
|
return undefined;
|
|
63
177
|
let submission;
|
|
@@ -122,6 +236,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
122
236
|
fn[submitHooksSymbol] = submitHooks;
|
|
123
237
|
fn[settledHooksSymbol] = settledHooks;
|
|
124
238
|
fn[invokeSymbol] = invoke;
|
|
239
|
+
installRouterIntegrations();
|
|
125
240
|
if (!isServer) {
|
|
126
241
|
actions.set(url, fn);
|
|
127
242
|
// Only remove the registration if it still belongs to this instance —
|
package/dist/data/events.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { RouterContext } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The submit delegation consults this slot instead of importing the action
|
|
4
|
+
* module: the action side installs its handler on first action creation
|
|
5
|
+
* (see data/action.ts), so an app that never creates an action never pulls
|
|
6
|
+
* the data layer into its bundle through the router's event wiring.
|
|
7
|
+
*/
|
|
8
|
+
export type RouterFormHandler = (evt: SubmitEvent, router: RouterContext, actionBase: string) => void;
|
|
9
|
+
export declare function setRouterFormHandler(handler: RouterFormHandler | undefined): void;
|
|
2
10
|
type NativeEventConfig = {
|
|
3
11
|
preload?: boolean;
|
|
4
12
|
explicitLinks?: boolean;
|
package/dist/data/events.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { delegateEvents } from "@solidjs/web";
|
|
2
2
|
import { onCleanup } from "solid-js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
let formHandler;
|
|
4
|
+
export function setRouterFormHandler(handler) {
|
|
5
|
+
formHandler = handler;
|
|
6
|
+
}
|
|
5
7
|
export function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) {
|
|
6
8
|
return (router) => {
|
|
7
9
|
const basePath = router.base.path();
|
|
@@ -76,30 +78,29 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
76
78
|
}, 20);
|
|
77
79
|
}
|
|
78
80
|
function handleFormSubmit(evt) {
|
|
81
|
+
if (formHandler)
|
|
82
|
+
return formHandler(evt, router, actionBase);
|
|
83
|
+
// No form handler means no action module in the client graph at all
|
|
84
|
+
// (e.g. server components binding forms straight to server functions).
|
|
85
|
+
// A POST to a url under actionBase is self-describing, so delegation
|
|
86
|
+
// is still sufficient: intercept synchronously — the no-JS treatment
|
|
87
|
+
// is reserved for clients with no JS — capture the FormData, and load
|
|
88
|
+
// the handler lazily. Apps that never submit one never load it.
|
|
79
89
|
if (evt.defaultPrevented)
|
|
80
90
|
return;
|
|
81
|
-
|
|
91
|
+
const form = evt.target;
|
|
92
|
+
const ref = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
82
93
|
? evt.submitter.getAttribute("formaction")
|
|
83
|
-
:
|
|
84
|
-
if (!
|
|
94
|
+
: form.getAttribute("action");
|
|
95
|
+
if (!ref || ref.startsWith("https://action/"))
|
|
85
96
|
return;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (evt.target.method.toUpperCase() !== "POST")
|
|
94
|
-
throw new Error("Only POST forms are supported for Actions");
|
|
95
|
-
const handler = actions.get(actionRef);
|
|
96
|
-
if (handler) {
|
|
97
|
-
evt.preventDefault();
|
|
98
|
-
const data = new FormData(evt.target, evt.submitter);
|
|
99
|
-
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
100
|
-
? data
|
|
101
|
-
: new URLSearchParams(data));
|
|
102
|
-
}
|
|
97
|
+
const url = new URL(ref, document.baseURI);
|
|
98
|
+
const path = router.parsePath(url.pathname + url.search);
|
|
99
|
+
if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST")
|
|
100
|
+
return;
|
|
101
|
+
evt.preventDefault();
|
|
102
|
+
const data = new FormData(form, evt.submitter);
|
|
103
|
+
import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
|
|
103
104
|
}
|
|
104
105
|
// ensure delegated event run first
|
|
105
106
|
delegateEvents(["click", "submit"]);
|
package/dist/data/flash.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { FLASH_COOKIE, hasFlashCookie, clearFlashCookie } from "./flashCookie.js";
|
|
2
2
|
/** What rides the cookie: a Submission minus its lifecycle methods. */
|
|
3
3
|
export interface FlashSubmission {
|
|
4
4
|
input: any[];
|
|
@@ -13,10 +13,6 @@ export interface FlashSubmission {
|
|
|
13
13
|
* `error`, returned values on `result` (mirroring the scripted split).
|
|
14
14
|
*/
|
|
15
15
|
export declare function encodeFlashCookie(url: string, result: any, input: any[], thrown?: boolean): string;
|
|
16
|
-
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
17
|
-
export declare function clearFlashCookie(): string;
|
|
18
|
-
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
19
|
-
export declare function hasFlashCookie(cookieHeader: string | null): boolean;
|
|
20
16
|
/**
|
|
21
17
|
* Decodes the flash cookie out of a request's Cookie header. Returns
|
|
22
18
|
* undefined when absent or unreadable (a malformed cookie must never take
|
package/dist/data/flash.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// The no-JS form convention's cookie codec. When a form posts to a server
|
|
2
2
|
// function without the client runtime (no instance header), the server
|
|
3
3
|
// handler redirects back carrying the outcome in a one-shot "flash" cookie;
|
|
4
|
-
// the next SSR pass reads it
|
|
4
|
+
// the next SSR pass reads it and seeds the router's submission state so
|
|
5
5
|
// useSubmission() renders the result exactly as a scripted submission would.
|
|
6
|
-
// Both halves live in this module so the write (src/server.ts, the
|
|
7
|
-
// handleNoJS) and the read (
|
|
8
|
-
// drift apart.
|
|
9
|
-
|
|
6
|
+
// Both codec halves live in this module so the write (src/server.ts, the
|
|
7
|
+
// handler's handleNoJS) and the read (provided to the router core by
|
|
8
|
+
// data/action.ts) can never drift apart. The cookie's name/clearing live in
|
|
9
|
+
// flashCookie.ts — the only piece the router core itself consumes — so this
|
|
10
|
+
// codec stays out of bundles that never load the action layer.
|
|
11
|
+
import { FLASH_COOKIE, matchFlashCookie } from "./flashCookie.js";
|
|
12
|
+
export { FLASH_COOKIE, hasFlashCookie, clearFlashCookie } from "./flashCookie.js";
|
|
10
13
|
// Form payloads have no JSON encoding, so entries are captured as pair
|
|
11
14
|
// arrays under a marker key ($f / $u) and revived to real FormData /
|
|
12
15
|
// URLSearchParams on the way out. File entries cannot ride a cookie and are
|
|
@@ -48,28 +51,17 @@ export function encodeFlashCookie(url, result, input, thrown) {
|
|
|
48
51
|
};
|
|
49
52
|
return `${FLASH_COOKIE}=${encodeURIComponent(JSON.stringify(payload))}; Secure; HttpOnly; Path=/`;
|
|
50
53
|
}
|
|
51
|
-
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
52
|
-
export function clearFlashCookie() {
|
|
53
|
-
return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
|
|
54
|
-
}
|
|
55
|
-
const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
|
|
56
|
-
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
57
|
-
export function hasFlashCookie(cookieHeader) {
|
|
58
|
-
return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
|
|
59
|
-
}
|
|
60
54
|
/**
|
|
61
55
|
* Decodes the flash cookie out of a request's Cookie header. Returns
|
|
62
56
|
* undefined when absent or unreadable (a malformed cookie must never take
|
|
63
57
|
* down SSR — it is cleared either way).
|
|
64
58
|
*/
|
|
65
59
|
export function decodeFlashCookie(cookieHeader) {
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
const match = cookieHeader.match(FLASH_MATCHER);
|
|
60
|
+
const match = matchFlashCookie(cookieHeader);
|
|
69
61
|
if (!match)
|
|
70
62
|
return;
|
|
71
63
|
try {
|
|
72
|
-
const payload = JSON.parse(decodeURIComponent(match
|
|
64
|
+
const payload = JSON.parse(decodeURIComponent(match));
|
|
73
65
|
if (!payload || !payload.result)
|
|
74
66
|
return;
|
|
75
67
|
const result = payload.error ? new Error(payload.result) : payload.result;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const FLASH_COOKIE = "flash";
|
|
2
|
+
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
3
|
+
export declare function hasFlashCookie(cookieHeader: string | null): boolean;
|
|
4
|
+
/** The raw encoded flash payload out of a Cookie header, if present. */
|
|
5
|
+
export declare function matchFlashCookie(cookieHeader: string | null): string | undefined;
|
|
6
|
+
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
7
|
+
export declare function clearFlashCookie(): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The flash cookie's name and one-shot clearing — split from the codec in
|
|
2
|
+
// flash.ts so the router core can consume the cookie eagerly (the clear must
|
|
3
|
+
// be appended before streaming flushes the response headers, and an unread
|
|
4
|
+
// outcome must not haunt a later request) without carrying the encode/decode
|
|
5
|
+
// machinery into client bundles that never load the action layer.
|
|
6
|
+
export const FLASH_COOKIE = "flash";
|
|
7
|
+
const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
|
|
8
|
+
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
9
|
+
export function hasFlashCookie(cookieHeader) {
|
|
10
|
+
return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
|
|
11
|
+
}
|
|
12
|
+
/** The raw encoded flash payload out of a Cookie header, if present. */
|
|
13
|
+
export function matchFlashCookie(cookieHeader) {
|
|
14
|
+
const match = cookieHeader && cookieHeader.match(FLASH_MATCHER);
|
|
15
|
+
return match ? match[1] : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
18
|
+
export function clearFlashCookie() {
|
|
19
|
+
return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { submitServerForm } from "./action.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The delegation fallback's lazy entry (see data/events.ts). Nothing imports
|
|
2
|
+
// this module statically — it exists so the dynamic import has a target that
|
|
3
|
+
// bundlers can keep as a split point: router-only apps never load the action
|
|
4
|
+
// machinery unless a server-action form actually submits.
|
|
5
|
+
export { submitServerForm } from "./action.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
|
-
export * from "./components.jsx";
|
|
3
2
|
export * from "./lifecycle.js";
|
|
4
|
-
export { useHref, useIsRouting, useLocation, useMatch,
|
|
3
|
+
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
|
+
export type { LinkState } from "./routing.js";
|
|
5
5
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
6
|
+
export { int } from "./paths.js";
|
|
7
|
+
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter } from "./paths.js";
|
|
6
8
|
export * from "./data/index.js";
|
|
7
|
-
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs } from "./types.js";
|
|
9
|
+
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|