@solidjs/router 2.0.0-next.26 → 2.0.0-next.28
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 +168 -70
- package/dist/data/liveQuery.d.ts +5 -5
- package/dist/data/liveQuery.js +13 -11
- package/dist/fs.d.ts +7 -1
- package/dist/fs.js +29 -5
- package/dist/fsServer.d.ts +15 -0
- package/dist/fsServer.js +10 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +247 -38
- package/dist/index.jsx +1 -0
- package/dist/routers/components.jsx +4 -0
- package/dist/routers/factory.d.ts +10 -1
- package/dist/routers/factory.jsx +14 -2
- package/dist/routing.js +65 -26
- package/dist/server.js +6 -0
- package/dist/serverRouteComponent.d.ts +20 -0
- package/dist/serverRouteComponent.js +90 -0
- package/dist/serverRouteShared.d.ts +33 -0
- package/dist/serverRouteShared.js +39 -0
- package/dist/types.d.ts +41 -4
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +7 -0
- package/package.json +8 -2
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate
|
|
|
23
23
|
export type { LinkState } from "./routing.js";
|
|
24
24
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
25
25
|
export { int } from "./paths.js";
|
|
26
|
+
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
26
27
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
27
28
|
export * from "./data/index.js";
|
|
28
|
-
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
|
29
|
+
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, ServerRouteArgs, ServerRouteParams, ServerRouteFunction, ServerRouteView, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
|
|
2
|
-
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
2
|
+
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
|
|
5
5
|
|
|
@@ -38,6 +38,13 @@ function invariant(value, message) {
|
|
|
38
38
|
function joinPaths(from, to) {
|
|
39
39
|
return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Run a Standard Schema synchronously — the only mode search params support. */
|
|
43
|
+
function validateSearch(schema, raw) {
|
|
44
|
+
const outcome = schema["~standard"].validate(raw);
|
|
45
|
+
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
46
|
+
return outcome;
|
|
47
|
+
}
|
|
41
48
|
function extractSearchParams(url) {
|
|
42
49
|
const params = {};
|
|
43
50
|
url.searchParams.forEach((value, key) => {
|
|
@@ -517,6 +524,64 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
517
524
|
return node(normalizePath(base));
|
|
518
525
|
}
|
|
519
526
|
|
|
527
|
+
// The light half of server component routes: the brand the router core reads
|
|
528
|
+
// off a route `component`, and the derivation of a route's call arguments.
|
|
529
|
+
// Deliberately free of `query`/`dynamic` — routing.ts imports this module,
|
|
530
|
+
// and routing.ts is in every app's eager graph. The heavy half (the helper
|
|
531
|
+
// that builds the branded component) lives in serverRouteComponent.ts and only
|
|
532
|
+
// enters a bundle when an app calls `serverRouteComponent()`.
|
|
533
|
+
|
|
534
|
+
const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What `serverRouteComponent()` attaches to the route component it returns, and
|
|
538
|
+
* what the router core drives it through: `call` is the query-wrapped server
|
|
539
|
+
* function (preload and single-flight collection warm/collect through it),
|
|
540
|
+
* `render` mounts the resolved server component for the current args with
|
|
541
|
+
* the route's props (`children` is the outlet).
|
|
542
|
+
*/
|
|
543
|
+
|
|
544
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
545
|
+
function serverRouteOf(component) {
|
|
546
|
+
return component?.[SERVER_ROUTE];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
551
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
552
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
553
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
554
|
+
*/
|
|
555
|
+
function serverRouteArgs(route, params, query) {
|
|
556
|
+
const schema = route.key?.search;
|
|
557
|
+
let search;
|
|
558
|
+
if (schema) {
|
|
559
|
+
const raw = {
|
|
560
|
+
...query
|
|
561
|
+
};
|
|
562
|
+
const outcome = validateSearch(schema, raw);
|
|
563
|
+
// Issues leave the raw values in place, matching useSearchParams: search
|
|
564
|
+
// strings are user input, so defaults belong in the schema itself.
|
|
565
|
+
search = outcome.issues ? raw : outcome.value;
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
params: {
|
|
569
|
+
...params
|
|
570
|
+
},
|
|
571
|
+
search
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const shallowEqual = (a, b) => a === b || !!a && !!b && typeof a === "object" && typeof b === "object" && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(k => a[k] === b[k]);
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
578
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
579
|
+
* must not re-enter the source (and refetch).
|
|
580
|
+
*/
|
|
581
|
+
function serverRouteArgsEqual(a, b) {
|
|
582
|
+
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
583
|
+
}
|
|
584
|
+
|
|
520
585
|
const MAX_REDIRECTS = 100;
|
|
521
586
|
|
|
522
587
|
/**
|
|
@@ -762,8 +827,7 @@ function useSearchParams(path) {
|
|
|
762
827
|
for (const match of router.matches()) {
|
|
763
828
|
const schema = match.route.key.search;
|
|
764
829
|
if (!schema) continue;
|
|
765
|
-
const outcome = schema
|
|
766
|
-
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
830
|
+
const outcome = validateSearch(schema, raw);
|
|
767
831
|
if (!outcome.issues) result = Object.assign(result || {
|
|
768
832
|
...raw
|
|
769
833
|
}, outcome.value);
|
|
@@ -1460,42 +1524,66 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1460
1524
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1461
1525
|
}
|
|
1462
1526
|
function preloadRoute(url, preloadData) {
|
|
1463
|
-
const
|
|
1527
|
+
const next = getRouteMatches(branches(), url.pathname);
|
|
1464
1528
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1465
1529
|
// component.preload (below) kicks the table load; once it lands,
|
|
1466
1530
|
// preload again so the real inner routes warm too. Preloads are
|
|
1467
1531
|
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
1468
1532
|
// here — the real navigation surfaces and retries it.
|
|
1469
|
-
const boundary =
|
|
1533
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1470
1534
|
if (boundary) {
|
|
1471
1535
|
try {
|
|
1472
1536
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1473
1537
|
} catch {}
|
|
1474
1538
|
}
|
|
1539
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
1540
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
1541
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
1542
|
+
// already this selective (a matching level is reused and re-reads through
|
|
1543
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
1544
|
+
let current;
|
|
1545
|
+
try {
|
|
1546
|
+
current = untrack(matches);
|
|
1547
|
+
} catch {}
|
|
1548
|
+
const query = extractSearchParams(url);
|
|
1549
|
+
const inputs = (p, q, s, r) => {
|
|
1550
|
+
const a = serverRouteArgs(r, p, q);
|
|
1551
|
+
a.search === undefined && (a.search = s);
|
|
1552
|
+
return a;
|
|
1553
|
+
};
|
|
1475
1554
|
const prevIntent = preloadIntent;
|
|
1476
1555
|
preloadIntent = "preload";
|
|
1477
|
-
for (let match in
|
|
1556
|
+
for (let match in next) {
|
|
1478
1557
|
const {
|
|
1479
1558
|
route,
|
|
1480
1559
|
params
|
|
1481
|
-
} =
|
|
1560
|
+
} = next[match];
|
|
1482
1561
|
route.component && route.component.preload && route.component.preload();
|
|
1483
1562
|
const {
|
|
1484
|
-
preload
|
|
1563
|
+
preload,
|
|
1564
|
+
component
|
|
1485
1565
|
} = route;
|
|
1566
|
+
const now = current && current[match];
|
|
1567
|
+
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1486
1568
|
inPreloadFn = true;
|
|
1487
|
-
preloadData &&
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1569
|
+
preloadData && !unchanged && runWithOwner(getContext(), () => {
|
|
1570
|
+
// A server component route's data IS its call: warm the same
|
|
1571
|
+
// query entry the render will read, under the same derived args.
|
|
1572
|
+
const server = serverRouteOf(component);
|
|
1573
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
1574
|
+
preload && preload({
|
|
1575
|
+
params,
|
|
1576
|
+
location: {
|
|
1577
|
+
pathname: url.pathname,
|
|
1578
|
+
search: url.search,
|
|
1579
|
+
hash: url.hash,
|
|
1580
|
+
query,
|
|
1581
|
+
state: null,
|
|
1582
|
+
key: ""
|
|
1583
|
+
},
|
|
1584
|
+
intent: "preload"
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1499
1587
|
inPreloadFn = false;
|
|
1500
1588
|
}
|
|
1501
1589
|
preloadIntent = prevIntent;
|
|
@@ -1551,14 +1639,30 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1551
1639
|
pattern,
|
|
1552
1640
|
params,
|
|
1553
1641
|
path,
|
|
1554
|
-
outlet: () =>
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1642
|
+
outlet: () => {
|
|
1643
|
+
if (!component) return outlet();
|
|
1644
|
+
const routeProps = {
|
|
1645
|
+
params,
|
|
1646
|
+
location,
|
|
1647
|
+
data,
|
|
1648
|
+
get children() {
|
|
1649
|
+
return outlet();
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
// A `serverRouteComponent()` route: the router derives the call from
|
|
1653
|
+
// the match — THIS level's params (never a child's) and the declared
|
|
1654
|
+
// search output — and mounts through the brand. Structural equality
|
|
1655
|
+
// keeps a navigation that leaves this level's args unchanged from
|
|
1656
|
+
// re-entering the call. See serverRouteComponent.ts.
|
|
1657
|
+
const server = serverRouteOf(component);
|
|
1658
|
+
if (server) {
|
|
1659
|
+
const args = createMemo(() => serverRouteArgs(match().route, match().params, location.query), {
|
|
1660
|
+
equals: serverRouteArgsEqual
|
|
1661
|
+
});
|
|
1662
|
+
return server.render(args, routeProps);
|
|
1560
1663
|
}
|
|
1561
|
-
|
|
1664
|
+
return createComponent(component, routeProps);
|
|
1665
|
+
},
|
|
1562
1666
|
resolvePath(to) {
|
|
1563
1667
|
return resolvePath(base.path(), to, path());
|
|
1564
1668
|
}
|
|
@@ -1717,6 +1821,9 @@ function dataOnly(event, routerState, branches) {
|
|
|
1717
1821
|
route,
|
|
1718
1822
|
params
|
|
1719
1823
|
} = matches[match];
|
|
1824
|
+
// a server component route's markup is collected like any query result
|
|
1825
|
+
const server = serverRouteOf(route.component);
|
|
1826
|
+
server && server.call(serverRouteArgs(route, params, routerState.location.query));
|
|
1720
1827
|
route.preload && route.preload({
|
|
1721
1828
|
params,
|
|
1722
1829
|
location: routerState.location,
|
|
@@ -2246,6 +2353,8 @@ function createRouter(config) {
|
|
|
2246
2353
|
};
|
|
2247
2354
|
const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
|
|
2248
2355
|
const matchPath = pathname => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
2356
|
+
// a `paths` node carries its logical pathname under the Href brand
|
|
2357
|
+
const pathnameOf = url => typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2249
2358
|
function RouterComponent(props) {
|
|
2250
2359
|
// One router per app: the session (location, history, delegation, link
|
|
2251
2360
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -2303,7 +2412,7 @@ function createRouter(config) {
|
|
|
2303
2412
|
routes: config.routes,
|
|
2304
2413
|
config,
|
|
2305
2414
|
match(url) {
|
|
2306
|
-
return matchPath(
|
|
2415
|
+
return matchPath(pathnameOf(url)).map(({
|
|
2307
2416
|
route,
|
|
2308
2417
|
path,
|
|
2309
2418
|
params
|
|
@@ -2314,6 +2423,17 @@ function createRouter(config) {
|
|
|
2314
2423
|
params,
|
|
2315
2424
|
info: route.info
|
|
2316
2425
|
}));
|
|
2426
|
+
},
|
|
2427
|
+
keysFor(url) {
|
|
2428
|
+
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2429
|
+
const keys = [];
|
|
2430
|
+
for (const {
|
|
2431
|
+
route
|
|
2432
|
+
} of matchPath(pathname)) {
|
|
2433
|
+
const key = serverRouteOf(route.component)?.call?.key;
|
|
2434
|
+
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
2435
|
+
}
|
|
2436
|
+
return keys;
|
|
2317
2437
|
}
|
|
2318
2438
|
});
|
|
2319
2439
|
// Built on first access (a getter via Object.assign would run during the
|
|
@@ -2406,6 +2526,93 @@ const useBeforeLeave = listener => {
|
|
|
2406
2526
|
onCleanup(s);
|
|
2407
2527
|
};
|
|
2408
2528
|
|
|
2529
|
+
// Server component routes (experimental — rides the experimental server
|
|
2530
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
2531
|
+
//
|
|
2532
|
+
// `serverRouteComponent(source)` turns a function of route arguments that
|
|
2533
|
+
// resolves to a component into a route `component`. It replaces the client
|
|
2534
|
+
// wrapper a server-component route used to need:
|
|
2535
|
+
//
|
|
2536
|
+
// // before: a client component exists only to make the call
|
|
2537
|
+
// const getStory = query(storyView, "story");
|
|
2538
|
+
// component: props => {
|
|
2539
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
2540
|
+
// return <View>{props.children}</View>;
|
|
2541
|
+
// }
|
|
2542
|
+
// // after
|
|
2543
|
+
// component: serverRouteComponent(query(storyView, "story"))
|
|
2544
|
+
//
|
|
2545
|
+
// The source is the app's: `query(fn, key)` for a request/response server
|
|
2546
|
+
// component, `liveQuery(fn, key)` for one that streams successive versions,
|
|
2547
|
+
// or any function of the args. The router does not choose the cache
|
|
2548
|
+
// strategy and does not own the key — `revalidate("story")` is the app's,
|
|
2549
|
+
// as it is for any query. What the router adds is the URL → call
|
|
2550
|
+
// translation: it derives the call's arguments from the match, mounts the
|
|
2551
|
+
// resolved component with the outlet as `children`, and calls the same
|
|
2552
|
+
// source under preload intent (link hover, `preloadRoute`, the single-flight
|
|
2553
|
+
// collector), so the query or live channel is warm before the navigation
|
|
2554
|
+
// renders against it.
|
|
2555
|
+
//
|
|
2556
|
+
// Arguments are derived, not read from a live location: the call's
|
|
2557
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
2558
|
+
// so the args must name precisely what the route depends on. They are the
|
|
2559
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
2560
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
2561
|
+
// when the route declares a `search` schema, its validated output.
|
|
2562
|
+
//
|
|
2563
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
2564
|
+
// A server component that takes other client positions — handlers, refs,
|
|
2565
|
+
// named slots — has a client half, and that half is a client component; the
|
|
2566
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
2567
|
+
/**
|
|
2568
|
+
* Use a server component as a route (experimental). `source` is a function
|
|
2569
|
+
* of the router-derived {@link ServerRouteArgs} — this route's `params`, and
|
|
2570
|
+
* `search` when the route declares a schema — resolving to a server
|
|
2571
|
+
* component: typically a `"use server"` function wrapped in `query()` or
|
|
2572
|
+
* `liveQuery()`, whose key the app names and revalidates. The router mounts
|
|
2573
|
+
* the result with the outlet as `children` and calls the same source under
|
|
2574
|
+
* preload intent, so link hover and single-flight collection warm it.
|
|
2575
|
+
*
|
|
2576
|
+
* `children` is the only client position the router fills, so the server
|
|
2577
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
2578
|
+
* slots has a client half — write an ordinary route component for it.
|
|
2579
|
+
*
|
|
2580
|
+
* ```ts
|
|
2581
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
2582
|
+
* ```
|
|
2583
|
+
*/
|
|
2584
|
+
function serverRouteComponent(
|
|
2585
|
+
// A source that ignores its args (an app shell) is `(...args: never[])`
|
|
2586
|
+
// once `query()` has typed it; that is the same call, so accept it too.
|
|
2587
|
+
source) {
|
|
2588
|
+
const call = source;
|
|
2589
|
+
|
|
2590
|
+
// The value is a component only so it fits the `component` field; the
|
|
2591
|
+
// router core never calls it — it mounts through the brand, with THIS
|
|
2592
|
+
// level's params and the declared search output. A direct mount (a
|
|
2593
|
+
// `lazy()` wrapper around the helper, a hand-written `<Route />`) would
|
|
2594
|
+
// have only the merged params to call with: the same view under a second
|
|
2595
|
+
// address, missing the cache and owning its own frame. Refuse it.
|
|
2596
|
+
const route = () => {
|
|
2597
|
+
throw new Error("serverRouteComponent(): mount it as a route's `component`" + (DEV ? " — the router derives the call from the match. It was rendered directly (through " + "lazy(), as the <Router> root, or outside a route). For an app shell, make it a " + "pathless layout route with children." : ""));
|
|
2598
|
+
};
|
|
2599
|
+
function render(args, routeProps) {
|
|
2600
|
+
// The source may answer a component, a promise of one, or (a live query)
|
|
2601
|
+
// successive components; `dynamic`'s memo lands each the same way.
|
|
2602
|
+
const View = dynamic(() => call(args()));
|
|
2603
|
+
return createComponent(View, {
|
|
2604
|
+
get children() {
|
|
2605
|
+
return routeProps.children;
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
route[SERVER_ROUTE] = {
|
|
2610
|
+
call,
|
|
2611
|
+
render
|
|
2612
|
+
};
|
|
2613
|
+
return route;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2409
2616
|
const LocationHeader = "Location";
|
|
2410
2617
|
const PRELOAD_TIMEOUT$1 = 5000;
|
|
2411
2618
|
const CACHE_TIMEOUT = 180000;
|
|
@@ -3389,10 +3596,12 @@ function push(ch, value) {
|
|
|
3389
3596
|
// connection is alive and is itself the freshness mechanism — a
|
|
3390
3597
|
// push-driven producer yields the mutated state on its own, and tearing
|
|
3391
3598
|
// down healthy connections on every mutation defeats the model.
|
|
3392
|
-
// -
|
|
3393
|
-
//
|
|
3394
|
-
//
|
|
3395
|
-
// for
|
|
3599
|
+
// - A single-flight payload carrying a live key delivers INTO the open
|
|
3600
|
+
// channel rather than reconnecting it. The router's own collector does not
|
|
3601
|
+
// produce live keys (the stream is the freshness mechanism; collection
|
|
3602
|
+
// for live server components is an open question — a mutation's region
|
|
3603
|
+
// would outrank the standing frame stream under the transport's version
|
|
3604
|
+
// policy), so this path serves an integration that collects them itself.
|
|
3396
3605
|
let hooked = false;
|
|
3397
3606
|
function hookRevalidate() {
|
|
3398
3607
|
if (hooked) return;
|
|
@@ -3438,11 +3647,11 @@ function hookRevalidate() {
|
|
|
3438
3647
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
3439
3648
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
3440
3649
|
* a first-connect failure.
|
|
3441
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
3442
|
-
*
|
|
3443
|
-
*
|
|
3444
|
-
*
|
|
3445
|
-
*
|
|
3650
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
3651
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
3652
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
3653
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
3654
|
+
* live key an integration collects itself.)
|
|
3446
3655
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
3447
3656
|
* it open through the preload window), so navigation renders against an
|
|
3448
3657
|
* already-connected stream instead of holding the transition on connect
|
|
@@ -3526,4 +3735,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
3526
3735
|
submitServerForm: submitServerForm
|
|
3527
3736
|
});
|
|
3528
3737
|
|
|
3529
|
-
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
|
3738
|
+
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
package/dist/index.jsx
CHANGED
|
@@ -3,4 +3,5 @@ export * from "./lifecycle.js";
|
|
|
3
3
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
4
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
5
5
|
export { int } from "./paths.js";
|
|
6
|
+
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
6
7
|
export * from "./data/index.js";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createMemo, createRoot, getOwner, onCleanup, runWithOwner, untrack, Show } from "solid-js";
|
|
3
3
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
4
|
import { createRouteContext, getIntent, getRouteMatches, resolveLazySubtree, RouteContextObj, setInPreloadFn, unresolvedLazyMatches } from "../routing.js";
|
|
5
|
+
import { serverRouteArgs, serverRouteOf } from "../serverRouteShared.js";
|
|
5
6
|
export function Root(props) {
|
|
6
7
|
const location = props.routerState.location;
|
|
7
8
|
const params = props.routerState.params;
|
|
@@ -129,6 +130,9 @@ function dataOnly(event, routerState, branches) {
|
|
|
129
130
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
130
131
|
event.router.dataOnly = true;
|
|
131
132
|
const { route, params } = matches[match];
|
|
133
|
+
// a server component route's markup is collected like any query result
|
|
134
|
+
const server = serverRouteOf(route.component);
|
|
135
|
+
server && server.call(serverRouteArgs(route, params, routerState.location.query));
|
|
132
136
|
route.preload &&
|
|
133
137
|
route.preload({
|
|
134
138
|
params,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { RoutePaths } from "../paths.js";
|
|
3
|
-
import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, ValidFilters } from "../types.js";
|
|
3
|
+
import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, TypedPath, ValidFilters } from "../types.js";
|
|
4
4
|
import type { RouterHistory } from "./history.js";
|
|
5
5
|
/**
|
|
6
6
|
* Identity helper that preserves literal types when the route tree is
|
|
@@ -116,6 +116,15 @@ export interface RouterInstance<R extends readonly RouteDefinition[] = RouteDefi
|
|
|
116
116
|
readonly config: RouterConfig<R>;
|
|
117
117
|
/** Pure matching against an arbitrary URL — no rendering or request context involved. Root→leaf; `[]` when nothing matches. */
|
|
118
118
|
match(url: string): OutputMatch[];
|
|
119
|
+
/**
|
|
120
|
+
* The query keys of the server component routes a URL shows, root→leaf —
|
|
121
|
+
* what to name in `reload`/`respond`/`redirect`'s `revalidate` (or the
|
|
122
|
+
* client's `revalidate()`) to refetch those routes and nothing else. Route
|
|
123
|
+
* keys, not addresses: the app's `query` key for a hand-written route, the
|
|
124
|
+
* file for a file-system route. Pure matching, so it works in a server
|
|
125
|
+
* action; a `paths` node is accepted.
|
|
126
|
+
*/
|
|
127
|
+
keysFor(url: string | TypedPath): string[];
|
|
119
128
|
}
|
|
120
129
|
export declare function createRouter<const R extends readonly RouteDefinition[]>(config: RouterConfig<R>): RouterInstance<R>;
|
|
121
130
|
export {};
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -7,7 +7,8 @@ import { DEV, OBSERVE } from "solid-js";
|
|
|
7
7
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
8
8
|
import { setupLinkClaims } from "../claims.js";
|
|
9
9
|
import { setupNativeEvents } from "../data/events.js";
|
|
10
|
-
import { createPathsProxy } from "../paths.js";
|
|
10
|
+
import { createPathsProxy, HREF } from "../paths.js";
|
|
11
|
+
import { serverRouteOf } from "../serverRouteShared.js";
|
|
11
12
|
import { createBranches, createRouterContext, getRouteMatches, mergeParams, registerFlightRouter, resolveLocationWrite, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
|
|
12
13
|
import { mockBase } from "../utils.js";
|
|
13
14
|
import { Root, Routes } from "./components.jsx";
|
|
@@ -186,6 +187,8 @@ export function createRouter(config) {
|
|
|
186
187
|
};
|
|
187
188
|
const renderPath = (config.history && config.history.utils && config.history.utils.renderPath) || undefined;
|
|
188
189
|
const matchPath = (pathname) => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
190
|
+
// a `paths` node carries its logical pathname under the Href brand
|
|
191
|
+
const pathnameOf = (url) => typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
189
192
|
function RouterComponent(props) {
|
|
190
193
|
// One router per app: the session (location, history, delegation, link
|
|
191
194
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -235,13 +238,22 @@ export function createRouter(config) {
|
|
|
235
238
|
routes: config.routes,
|
|
236
239
|
config,
|
|
237
240
|
match(url) {
|
|
238
|
-
return matchPath(
|
|
241
|
+
return matchPath(pathnameOf(url)).map(({ route, path, params }) => ({
|
|
239
242
|
path: route.originalPath,
|
|
240
243
|
pattern: route.pattern,
|
|
241
244
|
match: path,
|
|
242
245
|
params,
|
|
243
246
|
info: route.info
|
|
244
247
|
}));
|
|
248
|
+
},
|
|
249
|
+
keysFor(url) {
|
|
250
|
+
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
251
|
+
const keys = [];
|
|
252
|
+
for (const { route } of matchPath(pathname)) {
|
|
253
|
+
const key = serverRouteOf(route.component)?.call?.key;
|
|
254
|
+
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
255
|
+
}
|
|
256
|
+
return keys;
|
|
245
257
|
}
|
|
246
258
|
});
|
|
247
259
|
// Built on first access (a getter via Object.assign would run during the
|
package/dist/routing.js
CHANGED
|
@@ -4,8 +4,9 @@ import { runWithOwner } from "solid-js";
|
|
|
4
4
|
import { DEV } from "solid-js";
|
|
5
5
|
import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
|
|
6
6
|
import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
|
|
7
|
-
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
7
|
+
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
9
|
+
import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
|
|
9
10
|
const MAX_REDIRECTS = 100;
|
|
10
11
|
/**
|
|
11
12
|
* Resolves a write against `headed`, the location the router is heading to
|
|
@@ -196,9 +197,7 @@ export function useSearchParams(path) {
|
|
|
196
197
|
const schema = match.route.key.search;
|
|
197
198
|
if (!schema)
|
|
198
199
|
continue;
|
|
199
|
-
const outcome = schema
|
|
200
|
-
if (outcome instanceof Promise)
|
|
201
|
-
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
200
|
+
const outcome = validateSearch(schema, raw);
|
|
202
201
|
if (!outcome.issues)
|
|
203
202
|
result = Object.assign(result || { ...raw }, outcome.value);
|
|
204
203
|
}
|
|
@@ -880,42 +879,69 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
880
879
|
return ((to, options) => navigateFromRoute(route, to, options));
|
|
881
880
|
}
|
|
882
881
|
function preloadRoute(url, preloadData) {
|
|
883
|
-
const
|
|
882
|
+
const next = getRouteMatches(branches(), url.pathname);
|
|
884
883
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
885
884
|
// component.preload (below) kicks the table load; once it lands,
|
|
886
885
|
// preload again so the real inner routes warm too. Preloads are
|
|
887
886
|
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
888
887
|
// here — the real navigation surfaces and retries it.
|
|
889
|
-
const boundary =
|
|
888
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
890
889
|
if (boundary) {
|
|
891
890
|
try {
|
|
892
891
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
|
|
893
892
|
}
|
|
894
893
|
catch { }
|
|
895
894
|
}
|
|
895
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
896
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
897
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
898
|
+
// already this selective (a matching level is reused and re-reads through
|
|
899
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
900
|
+
let current;
|
|
901
|
+
try {
|
|
902
|
+
current = untrack(matches);
|
|
903
|
+
}
|
|
904
|
+
catch { }
|
|
905
|
+
const query = extractSearchParams(url);
|
|
906
|
+
const inputs = (p, q, s, r) => {
|
|
907
|
+
const a = serverRouteArgs(r, p, q);
|
|
908
|
+
a.search === undefined && (a.search = s);
|
|
909
|
+
return a;
|
|
910
|
+
};
|
|
896
911
|
const prevIntent = preloadIntent;
|
|
897
912
|
preloadIntent = "preload";
|
|
898
|
-
for (let match in
|
|
899
|
-
const { route, params } =
|
|
913
|
+
for (let match in next) {
|
|
914
|
+
const { route, params } = next[match];
|
|
900
915
|
route.component &&
|
|
901
916
|
route.component.preload &&
|
|
902
917
|
route.component.preload();
|
|
903
|
-
const { preload } = route;
|
|
918
|
+
const { preload, component } = route;
|
|
919
|
+
const now = current && current[match];
|
|
920
|
+
const unchanged = now &&
|
|
921
|
+
now.route.key === route.key &&
|
|
922
|
+
serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
904
923
|
inPreloadFn = true;
|
|
905
924
|
preloadData &&
|
|
906
|
-
|
|
907
|
-
runWithOwner(getContext(), () =>
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
925
|
+
!unchanged &&
|
|
926
|
+
runWithOwner(getContext(), () => {
|
|
927
|
+
// A server component route's data IS its call: warm the same
|
|
928
|
+
// query entry the render will read, under the same derived args.
|
|
929
|
+
const server = serverRouteOf(component);
|
|
930
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
931
|
+
preload &&
|
|
932
|
+
preload({
|
|
933
|
+
params,
|
|
934
|
+
location: {
|
|
935
|
+
pathname: url.pathname,
|
|
936
|
+
search: url.search,
|
|
937
|
+
hash: url.hash,
|
|
938
|
+
query,
|
|
939
|
+
state: null,
|
|
940
|
+
key: ""
|
|
941
|
+
},
|
|
942
|
+
intent: "preload"
|
|
943
|
+
});
|
|
944
|
+
});
|
|
919
945
|
inPreloadFn = false;
|
|
920
946
|
}
|
|
921
947
|
preloadIntent = prevIntent;
|
|
@@ -965,16 +991,29 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
965
991
|
pattern,
|
|
966
992
|
params,
|
|
967
993
|
path,
|
|
968
|
-
outlet: () =>
|
|
969
|
-
|
|
994
|
+
outlet: () => {
|
|
995
|
+
if (!component)
|
|
996
|
+
return outlet();
|
|
997
|
+
const routeProps = {
|
|
970
998
|
params,
|
|
971
999
|
location,
|
|
972
1000
|
data,
|
|
973
1001
|
get children() {
|
|
974
1002
|
return outlet();
|
|
975
1003
|
}
|
|
976
|
-
}
|
|
977
|
-
|
|
1004
|
+
};
|
|
1005
|
+
// A `serverRouteComponent()` route: the router derives the call from
|
|
1006
|
+
// the match — THIS level's params (never a child's) and the declared
|
|
1007
|
+
// search output — and mounts through the brand. Structural equality
|
|
1008
|
+
// keeps a navigation that leaves this level's args unchanged from
|
|
1009
|
+
// re-entering the call. See serverRouteComponent.ts.
|
|
1010
|
+
const server = serverRouteOf(component);
|
|
1011
|
+
if (server) {
|
|
1012
|
+
const args = createMemo(() => serverRouteArgs(match().route, match().params, location.query), { equals: serverRouteArgsEqual });
|
|
1013
|
+
return server.render(args, routeProps);
|
|
1014
|
+
}
|
|
1015
|
+
return createComponent(component, routeProps);
|
|
1016
|
+
},
|
|
978
1017
|
resolvePath(to) {
|
|
979
1018
|
return resolvePath(base.path(), to, path());
|
|
980
1019
|
}
|