@solidjs/router 0.17.0-next.5 → 1.0.0-next.7

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 (49) hide show
  1. package/README.md +526 -716
  2. package/dist/claims.d.ts +21 -0
  3. package/dist/claims.js +108 -0
  4. package/dist/data/action.d.ts +28 -1
  5. package/dist/data/action.js +184 -43
  6. package/dist/data/events.d.ts +8 -0
  7. package/dist/data/events.js +23 -22
  8. package/dist/data/flash.d.ts +21 -0
  9. package/dist/data/flash.js +78 -0
  10. package/dist/data/flashCookie.d.ts +7 -0
  11. package/dist/data/flashCookie.js +20 -0
  12. package/dist/data/query.js +11 -4
  13. package/dist/data/serverForms.d.ts +1 -0
  14. package/dist/data/serverForms.js +5 -0
  15. package/dist/index.d.ts +5 -3
  16. package/dist/index.js +1381 -686
  17. package/dist/index.jsx +2 -2
  18. package/dist/lifecycle.d.ts +29 -4
  19. package/dist/lifecycle.js +40 -37
  20. package/dist/paths.d.ts +117 -0
  21. package/dist/paths.js +41 -0
  22. package/dist/routers/components.d.ts +10 -21
  23. package/dist/routers/components.jsx +29 -44
  24. package/dist/routers/factory.d.ts +45 -0
  25. package/dist/routers/factory.jsx +143 -0
  26. package/dist/routers/history.d.ts +24 -0
  27. package/dist/routers/history.js +180 -0
  28. package/dist/routers/index.d.ts +4 -11
  29. package/dist/routers/index.js +2 -6
  30. package/dist/routing.d.ts +80 -44
  31. package/dist/routing.js +303 -107
  32. package/dist/server.d.ts +75 -0
  33. package/dist/server.js +264 -0
  34. package/dist/types.d.ts +75 -6
  35. package/dist/utils.d.ts +2 -0
  36. package/dist/utils.js +2 -0
  37. package/package.json +10 -8
  38. package/dist/components.d.ts +0 -31
  39. package/dist/components.jsx +0 -46
  40. package/dist/routers/HashRouter.d.ts +0 -9
  41. package/dist/routers/HashRouter.js +0 -41
  42. package/dist/routers/MemoryRouter.d.ts +0 -24
  43. package/dist/routers/MemoryRouter.js +0 -57
  44. package/dist/routers/Router.d.ts +0 -9
  45. package/dist/routers/Router.js +0 -45
  46. package/dist/routers/StaticRouter.d.ts +0 -6
  47. package/dist/routers/StaticRouter.js +0 -15
  48. package/dist/routers/createRouter.d.ts +0 -10
  49. package/dist/routers/createRouter.js +0 -40
package/dist/server.js ADDED
@@ -0,0 +1,264 @@
1
+ // The router's server-side integration with the server function runtime
2
+ // (@solidjs/web/server-functions). Server-only entry (`@solidjs/router/server`):
3
+ // it reaches for node async context via @solidjs/web/storage — keep it out of
4
+ // client bundles.
5
+ //
6
+ // Two policies live here, both previously implemented inside SolidStart and
7
+ // now owned by the router (the vocabulary — query keys, submissions — is the
8
+ // router's):
9
+ //
10
+ // - single-flight mutations: `createFlightDataCollector` produces the
11
+ // `collectFlightData` hook the app hands to
12
+ // `configureServerFunctionsServer` (or the handler options). After a
13
+ // mutation it produces the route data the mutation invalidated for the
14
+ // page the client is on (or is being redirected to), which the handler
15
+ // folds into the response — mutation and fresh data in one round trip.
16
+ // - the no-JS form convention: `createNoJSHandler` produces the
17
+ // `handleNoJS` hook — form posts made without the client runtime redirect
18
+ // back (303) carrying the outcome in a one-shot flash cookie that the
19
+ // router's SSR initialization reads into submission state.
20
+ import { provideRequestEvent } from "@solidjs/web/storage";
21
+ import { createBranches, getRouteMatches, mergeParams, peekLazySubtrees, resolveLazySubtree } from "./routing.js";
22
+ import { extractSearchParams } from "./utils.js";
23
+ import { encodeFlashCookie } from "./data/flash.js";
24
+ // the instance is the provider component, so it (unlike an options object) is a function
25
+ function isRouterInstance(options) {
26
+ return typeof options === "function";
27
+ }
28
+ /**
29
+ * Produces the `collectFlightData` implementation for
30
+ * `configureServerFunctionsServer` (or `handleServerFunctionRequest`
31
+ * options). Accepts a `createRouter` instance directly — its routes, base,
32
+ * and `preload` are the single source of truth — or an options object for
33
+ * trees not created through the factory.
34
+ *
35
+ * Strategy: rerun the route data for the URL the client will show
36
+ * after the mutation — the redirect `Location` when the outcome carries
37
+ * one, the referring page otherwise — collecting each `query` result under
38
+ * its cache key, scoped to the outcome's `X-Revalidate` keys when present
39
+ * (routes newly entered via redirect always collect fully). The returned
40
+ * payload seeds the client router's cache through its registered
41
+ * flight-data consumer.
42
+ */
43
+ export function createFlightDataCollector(options) {
44
+ const { routes, rootPreload, base = "" } = isRouterInstance(options)
45
+ ? { routes: options.routes, rootPreload: options.config.preload, base: options.config.base }
46
+ : options;
47
+ if (!routes)
48
+ throw new Error("createFlightDataCollector requires `routes`");
49
+ let branches;
50
+ let compiledVersion = -1;
51
+ // thunks are called at first collection so per-request trees build lazily;
52
+ // recompiled when a lazy subtree resolves (shared, append-only state)
53
+ const resolveBranches = () => {
54
+ const version = peekLazySubtrees();
55
+ if (!branches || compiledVersion !== version) {
56
+ branches = createBranches(typeof routes === "function" ? routes() : routes, base);
57
+ compiledVersion = version;
58
+ }
59
+ return branches;
60
+ };
61
+ return async (sourceEvent, outcome) => {
62
+ // no referrer, nothing to produce data for (e.g. non-browser callers)
63
+ const referrer = outcome.request.headers.get("referer");
64
+ if (!referrer)
65
+ return undefined;
66
+ // a raw body-carrying Response is the caller's verbatim payload — there
67
+ // is no envelope to fold data into
68
+ if (outcome.value instanceof Response && outcome.value.body)
69
+ return undefined;
70
+ const origin = new URL(outcome.request.url).origin;
71
+ let revalidate;
72
+ let url = new URL(referrer).toString();
73
+ if (outcome.response) {
74
+ if (outcome.response.headers.has("X-Revalidate"))
75
+ revalidate = outcome.response.headers.get("X-Revalidate").split(",");
76
+ if (outcome.response.headers.has("Location"))
77
+ url = new URL(outcome.response.headers.get("Location"), origin + base).toString();
78
+ }
79
+ // redirects leaving the app can't be collected for
80
+ if (new URL(url).origin !== origin)
81
+ return undefined;
82
+ // The flight event: the source event (its `response` rides along, so
83
+ // cookies/headers set during preloads still reach the real response)
84
+ // pointed at the target URL, with the mutation's own cookie mutations
85
+ // folded into the request, in data-only router mode.
86
+ const event = { ...sourceEvent };
87
+ event.request = new Request(url, { headers: createSingleFlightHeaders(sourceEvent) });
88
+ event.router = {
89
+ dataOnly: revalidate || true,
90
+ previousUrl: referrer,
91
+ data: {}
92
+ };
93
+ return provideRequestEvent(event, async () => {
94
+ try {
95
+ await resolveLazyMatches(resolveBranches, url, referrer);
96
+ runPreloads(event, resolveBranches(), url, referrer, rootPreload);
97
+ }
98
+ catch (error) {
99
+ console.error(error);
100
+ }
101
+ const data = event.router.data;
102
+ if (!data)
103
+ return undefined;
104
+ let containsKey = false;
105
+ for (const key in data) {
106
+ if (data[key] === undefined)
107
+ delete data[key];
108
+ else
109
+ containsKey = true;
110
+ }
111
+ return containsKey ? data : undefined;
112
+ });
113
+ };
114
+ }
115
+ // Lazy subtrees matched by the target (or previous) URL must resolve before
116
+ // the preload pass — the collector would otherwise be blind to inner routes'
117
+ // data. Resolution is shared with the client-side machinery (append-only,
118
+ // cached per thunk); the loop handles boundaries nested inside boundaries.
119
+ async function resolveLazyMatches(resolveBranches, url, previousUrl) {
120
+ for (;;) {
121
+ const branches = resolveBranches();
122
+ const target = new URL(url);
123
+ const matches = [
124
+ ...getRouteMatches(branches, target.pathname),
125
+ ...getRouteMatches(branches, new URL(previousUrl, target).pathname)
126
+ ];
127
+ const pending = matches.filter(m => m.route.lazy && !m.route.lazy.resolved);
128
+ if (!pending.length)
129
+ return;
130
+ await Promise.all(pending.map(m => resolveLazySubtree(m.route.lazy)));
131
+ }
132
+ }
133
+ // The pure preload runner: the same collection loop the router's <Routes>
134
+ // component runs when it detects a data-only render (routers/components.tsx),
135
+ // driven directly off the route tree — matched routes' preloads run and
136
+ // their `query` calls store results on `event.router.data`. Routes not
137
+ // matched by the previous URL flip collection from the revalidation-key
138
+ // filter to everything (their queries have no client cache yet).
139
+ function runPreloads(event, branches, url, previousUrl, rootPreload) {
140
+ const target = new URL(url);
141
+ const prevMatches = getRouteMatches(branches, new URL(previousUrl, target).pathname);
142
+ const matches = getRouteMatches(branches, target.pathname);
143
+ const location = {
144
+ pathname: target.pathname,
145
+ search: target.search,
146
+ hash: target.hash,
147
+ query: extractSearchParams(target),
148
+ state: null,
149
+ key: ""
150
+ };
151
+ // the root layout preloads first, exactly as <Root> would during a real
152
+ // server render: merged params of every match, intent "initial" (there is
153
+ // no navigation intent on the server), before any dataOnly-filter flip
154
+ rootPreload && rootPreload({ params: mergeParams(matches), location, intent: "initial" });
155
+ for (let match = 0; match < matches.length; match++) {
156
+ if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
157
+ event.router.dataOnly = true;
158
+ const { route, params } = matches[match];
159
+ route.preload &&
160
+ route.preload({
161
+ params,
162
+ location,
163
+ intent: "preload"
164
+ });
165
+ }
166
+ }
167
+ /**
168
+ * The request headers for the flight-data collection pass: the source
169
+ * request's headers with the mutation's `Set-Cookie` mutations folded into
170
+ * the `Cookie` header, so preloads observe the post-mutation cookie state
171
+ * (deletions honored via Max-Age/Expires).
172
+ */
173
+ export function createSingleFlightHeaders(sourceEvent) {
174
+ const headers = new Headers(sourceEvent.request.headers);
175
+ const setCookies = sourceEvent.response?.headers?.getSetCookie() ?? [];
176
+ if (!setCookies.length)
177
+ return headers;
178
+ const cookies = {};
179
+ for (const pair of headers.get("cookie")?.split(";") ?? []) {
180
+ const eq = pair.indexOf("=");
181
+ if (eq > -1)
182
+ cookies[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
183
+ }
184
+ for (const setCookie of setCookies) {
185
+ const parsed = parseSetCookie(setCookie);
186
+ if (!parsed)
187
+ continue;
188
+ if ((parsed.maxAge != null && parsed.maxAge <= 0) ||
189
+ (parsed.expires != null && parsed.expires.getTime() <= Date.now())) {
190
+ delete cookies[parsed.name];
191
+ }
192
+ else {
193
+ cookies[parsed.name] = parsed.value;
194
+ }
195
+ }
196
+ headers.delete("cookie");
197
+ const serialized = Object.entries(cookies)
198
+ .map(([name, value]) => `${name}=${value}`)
199
+ .join("; ");
200
+ if (serialized)
201
+ headers.set("cookie", serialized);
202
+ return headers;
203
+ }
204
+ function parseSetCookie(setCookie) {
205
+ const [pair, ...attributes] = setCookie.split(";");
206
+ const eq = pair.indexOf("=");
207
+ if (eq < 0)
208
+ return undefined;
209
+ const parsed = {
210
+ name: pair.slice(0, eq).trim(),
211
+ value: pair.slice(eq + 1).trim()
212
+ };
213
+ for (const attribute of attributes) {
214
+ const attrEq = attribute.indexOf("=");
215
+ const key = (attrEq < 0 ? attribute : attribute.slice(0, attrEq)).trim().toLowerCase();
216
+ const value = attrEq < 0 ? "" : attribute.slice(attrEq + 1).trim();
217
+ if (key === "max-age")
218
+ parsed.maxAge = Number(value);
219
+ else if (key === "expires")
220
+ parsed.expires = new Date(value);
221
+ }
222
+ return parsed;
223
+ }
224
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages
225
+ const validRedirectStatuses = new Set([301, 302, 303, 307, 308]);
226
+ /**
227
+ * Produces the `handleNoJS` implementation for
228
+ * `handleServerFunctionRequest` options: form posts made without the client
229
+ * runtime redirect back to the referring page (or to the result's
230
+ * `Location`) with the outcome riding a one-shot `flash` cookie. The
231
+ * router's SSR initialization reads the cookie into submission state, so
232
+ * `useSubmission()` renders the result exactly as a scripted submission
233
+ * would — progressive enhancement with no app code.
234
+ */
235
+ export function createNoJSHandler(options = {}) {
236
+ const { base = "" } = options;
237
+ return function handleNoJS(result, request, args, thrown) {
238
+ const url = new URL(request.url);
239
+ const back = request.headers.get("referer") || url.origin + base;
240
+ // form post -> GET: 303 See Other unless the result names a redirect status
241
+ let status = 303;
242
+ let headers;
243
+ if (result instanceof Response) {
244
+ headers = new Headers(result.headers);
245
+ if (result.headers.has("Location")) {
246
+ headers.set("Location", new URL(result.headers.get("Location"), url.origin + base).toString());
247
+ if (validRedirectStatuses.has(result.status))
248
+ status = result.status;
249
+ }
250
+ else {
251
+ headers.set("Location", back);
252
+ }
253
+ }
254
+ else {
255
+ headers = new Headers({ Location: back });
256
+ }
257
+ // Responses carry their meaning in their metadata; anything else flashes
258
+ // the outcome for the next render's submission state.
259
+ if (result && !(result instanceof Response)) {
260
+ headers.append("Set-Cookie", encodeFlashCookie(url.pathname + url.search, result, args, thrown));
261
+ }
262
+ return new Response(null, { status, headers });
263
+ };
264
+ }
package/dist/types.d.ts CHANGED
@@ -12,7 +12,7 @@ declare module "@solidjs/web" {
12
12
  cache?: Map<string, CacheEntry>;
13
13
  submission?: {
14
14
  input: any;
15
- result: any;
15
+ result?: any;
16
16
  error?: any;
17
17
  url: string;
18
18
  };
@@ -25,6 +25,42 @@ declare module "@solidjs/web" {
25
25
  }
26
26
  export type Params = Record<string, string | undefined>;
27
27
  export type SearchParams = Record<string, string | string[] | undefined>;
28
+ declare const PATH_PARAMS: unique symbol;
29
+ export interface TypedPath<P extends Params = Params> extends JSX.SerializableAttributeValue {
30
+ readonly [PATH_PARAMS]: P;
31
+ toString(): string;
32
+ }
33
+ declare const PATH_SEARCH: unique symbol;
34
+ export interface TypedSearchPath<In = SetSearchParams, Out = SearchParams> {
35
+ readonly [PATH_SEARCH]: {
36
+ input: In;
37
+ output: Out;
38
+ };
39
+ }
40
+ /**
41
+ * The Standard Schema interface (https://standardschema.dev) — vendored as a
42
+ * type so any conforming validator (Valibot, Zod, ArkType, ...) can type a
43
+ * route's `search` without the router depending on a validation library.
44
+ */
45
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
46
+ readonly "~standard": {
47
+ readonly version: 1;
48
+ readonly vendor: string;
49
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
50
+ readonly types?: {
51
+ readonly input: Input;
52
+ readonly output: Output;
53
+ } | undefined;
54
+ };
55
+ }
56
+ export type StandardSchemaResult<Output> = {
57
+ readonly value: Output;
58
+ readonly issues?: undefined;
59
+ } | {
60
+ readonly issues: ReadonlyArray<{
61
+ readonly message: string;
62
+ }>;
63
+ };
28
64
  export type SetParams = Record<string, string | number | boolean | null | undefined>;
29
65
  export type SetSearchParams = Record<string, string | string[] | number | number[] | boolean | boolean[] | null | undefined>;
30
66
  export interface Path {
@@ -44,7 +80,7 @@ export interface NavigateOptions<S = unknown> {
44
80
  state: S;
45
81
  }
46
82
  export interface Navigator {
47
- (to: string | number, options?: Partial<NavigateOptions>): void;
83
+ (to: string | TypedPath | number, options?: Partial<NavigateOptions>): void;
48
84
  (delta: number): void;
49
85
  }
50
86
  export type NavigatorFactory = (route?: RouteContext) => Navigator;
@@ -57,7 +93,6 @@ export interface LocationChange<S = unknown> {
57
93
  }
58
94
  export interface RouterIntegration {
59
95
  signal: [get: () => LocationChange, set: (next: LocationChange) => void];
60
- create?: (router: RouterContext) => void;
61
96
  utils?: Partial<RouterUtils>;
62
97
  }
63
98
  export type Intent = "initial" | "native" | "navigate" | "preload";
@@ -74,12 +109,28 @@ export interface RouteSectionProps<T = unknown> {
74
109
  children?: JSX.Element;
75
110
  }
76
111
  export type RouteSectionComponent<T = unknown> = Component<RouteSectionProps<T>> | Component<Omit<RouteSectionProps<T>, "children">> | Component<{}>;
112
+ /**
113
+ * A lazy route subtree: a thunk producing the child definitions — typically
114
+ * `() => import("./feature/routes")` (the module's `default` or `routes`
115
+ * export is used). The subtree's code loads on demand (hover intent or
116
+ * navigation kicks it, and the load folds into the navigation transition)
117
+ * but its *types* still flow into `paths` through the import's promise type.
118
+ * Thunks must be deterministic — resolution is cached append-only and shared
119
+ * by every mount, request, and the server's flight collector.
120
+ */
121
+ export type LazyRouteChildren = () => readonly RouteDefinition[] | Promise<readonly RouteDefinition[] | {
122
+ default: readonly RouteDefinition[];
123
+ } | {
124
+ routes: readonly RouteDefinition[];
125
+ }>;
77
126
  export type RouteDefinition<S extends string | string[] = any, T = any> = {
78
127
  path?: S;
79
128
  matchFilters?: MatchFilters<S>;
80
129
  preload?: RoutePreloadFunc<T>;
81
- children?: RouteDefinition | RouteDefinition[];
130
+ children?: RouteDefinition | readonly RouteDefinition[] | LazyRouteChildren;
82
131
  component?: RouteSectionComponent<T>;
132
+ /** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
133
+ search?: StandardSchemaV1<any, any>;
83
134
  info?: Record<string, any>;
84
135
  };
85
136
  export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
@@ -110,6 +161,14 @@ export interface RouteDescription {
110
161
  matcher: (location: string) => PathMatch | null;
111
162
  matchFilters?: MatchFilters;
112
163
  info?: Record<string, any>;
164
+ /** Present on the placeholder that stands in for an unresolved lazy subtree. */
165
+ lazy?: LazyBoundary;
166
+ }
167
+ /** Resolution state for a lazy subtree boundary, shared per thunk. */
168
+ export interface LazyBoundary {
169
+ thunk: LazyRouteChildren;
170
+ promise?: Promise<readonly RouteDefinition[]>;
171
+ resolved?: readonly RouteDefinition[];
113
172
  }
114
173
  export interface Branch {
115
174
  routes: RouteDescription[];
@@ -129,7 +188,7 @@ export interface RouterUtils {
129
188
  renderPath(path: string): string;
130
189
  parsePath(str: string): string;
131
190
  go(delta: number): void;
132
- beforeLeave: BeforeLeaveLifecycle;
191
+ beforeLeave: BeforeLeaveSlot;
133
192
  paramsWrapper: (getParams: () => Params, branches: () => Branch[]) => Params;
134
193
  queryWrapper: (getQuery: () => SearchParams) => SearchParams;
135
194
  }
@@ -145,7 +204,7 @@ export interface RouterContext {
145
204
  matches: () => RouteMatch[];
146
205
  renderPath(path: string): string;
147
206
  parsePath(str: string): string;
148
- beforeLeave: BeforeLeaveLifecycle;
207
+ beforeLeave: BeforeLeaveSlot;
149
208
  preloadRoute: (url: URL, preloadData?: boolean) => void;
150
209
  singleFlight: boolean;
151
210
  submissions: Signal<Submission<any, any>[]>;
@@ -167,6 +226,15 @@ export interface BeforeLeaveLifecycle {
167
226
  subscribe(listener: BeforeLeaveListener): () => void;
168
227
  confirm(to: string | number, options?: Partial<NavigateOptions>): boolean;
169
228
  }
229
+ /**
230
+ * Lazily-filled holder for the beforeLeave guard. The router and history
231
+ * adapters only carry this empty object; `useBeforeLeave` installs the actual
232
+ * lifecycle on first use so its machinery tree-shakes out of apps that never
233
+ * block navigation.
234
+ */
235
+ export interface BeforeLeaveSlot {
236
+ current?: BeforeLeaveLifecycle;
237
+ }
170
238
  export type Submission<T, U> = {
171
239
  readonly input: T;
172
240
  readonly result?: U;
@@ -182,3 +250,4 @@ export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<
182
250
  count: number;
183
251
  }];
184
252
  export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
253
+ export {};
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
2
2
  export declare const mockBase = "http://sr";
3
3
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
4
+ /** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
5
+ export declare const comparablePath: (path: string) => string;
4
6
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
5
7
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
6
8
  export declare function joinPaths(from: string, to: string): string;
package/dist/utils.js CHANGED
@@ -6,6 +6,8 @@ export function normalizePath(path, omitSlash = false) {
6
6
  const s = path.replace(trimPathRegex, "$1");
7
7
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
8
8
  }
9
+ /** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
10
+ export const comparablePath = (path) => normalizePath(path.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
9
11
  export function resolvePath(base, path, from) {
10
12
  if (hasSchemeRegex.test(path)) {
11
13
  return undefined;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.17.0-next.5",
9
+ "version": "1.0.0-next.7",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -22,14 +22,15 @@
22
22
  ".": {
23
23
  "solid": "./dist/index.jsx",
24
24
  "default": "./dist/index.js"
25
- }
25
+ },
26
+ "./server": "./dist/server.js"
26
27
  },
27
28
  "files": [
28
29
  "dist"
29
30
  ],
30
31
  "sideEffects": false,
31
32
  "devDependencies": {
32
- "@solidjs/web": "2.0.0-beta.19",
33
+ "@solidjs/web": "2.0.0-beta.22",
33
34
  "@babel/core": "^7.26.0",
34
35
  "@babel/preset-typescript": "^7.26.0",
35
36
  "@changesets/cli": "^2.27.10",
@@ -38,24 +39,25 @@
38
39
  "@rollup/plugin-terser": "0.4.4",
39
40
  "@types/jest": "^29.5.14",
40
41
  "@types/node": "^22.10.0",
41
- "babel-preset-solid": "2.0.0-beta.19",
42
+ "babel-preset-solid": "2.0.0-beta.22",
42
43
  "jsdom": "^25.0.1",
43
44
  "prettier": "^3.4.1",
44
45
  "rollup": "^4.27.4",
45
- "solid-js": "2.0.0-beta.19",
46
+ "solid-js": "2.0.0-beta.22",
46
47
  "typescript": "^5.7.2",
47
48
  "vite": "^6.0.0",
48
49
  "vite-plugin-solid": "3.0.0-next.10",
49
50
  "vitest": "^2.1.6"
50
51
  },
51
52
  "peerDependencies": {
52
- "@solidjs/web": ">=2.0.0-beta.19 <2.0.0-experimental.0",
53
- "solid-js": ">=2.0.0-beta.19 <2.0.0-experimental.0"
53
+ "@solidjs/web": ">=2.0.0-beta.22 <2.0.0-experimental.0",
54
+ "solid-js": ">=2.0.0-beta.22 <2.0.0-experimental.0"
54
55
  },
55
56
  "scripts": {
56
57
  "build": "rm -rf dist && tsc && rollup -c",
57
- "test": "vitest run && npm run test:types",
58
+ "test": "vitest run && vitest run --config vitest.config.server.ts && npm run test:types",
58
59
  "test:watch": "vitest",
60
+ "test:server": "vitest run --config vitest.config.server.ts",
59
61
  "test:types": "tsc --project tsconfig.test.json",
60
62
  "pretty": "prettier --write \"{src,test}/**/*.{ts,tsx}\"",
61
63
  "release": "pnpm build && changeset publish"
@@ -1,31 +0,0 @@
1
- import type { JSX } from "@solidjs/web";
2
- import type { Location, Navigator } from "./types.js";
3
- declare module "@solidjs/web/jsx-runtime" {
4
- namespace JSX {
5
- interface AnchorHTMLAttributes<T> {
6
- state?: string;
7
- noScroll?: boolean;
8
- replace?: boolean;
9
- preload?: boolean;
10
- link?: boolean;
11
- }
12
- }
13
- }
14
- export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
15
- href: string;
16
- replace?: boolean | undefined;
17
- noScroll?: boolean | undefined;
18
- state?: unknown | undefined;
19
- inactiveClass?: string | undefined;
20
- activeClass?: string | undefined;
21
- end?: boolean | undefined;
22
- }
23
- export declare function A(props: AnchorProps): JSX.Element;
24
- export interface NavigateProps {
25
- href: ((args: {
26
- navigate: Navigator;
27
- location: Location;
28
- }) => string) | string;
29
- state?: unknown;
30
- }
31
- export declare function Navigate(props: NavigateProps): null;
@@ -1,46 +0,0 @@
1
- import { createMemo, merge, omit } from "solid-js";
2
- import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
3
- import { normalizePath } from "./utils.js";
4
- function toClassName(value) {
5
- if (!value)
6
- return "";
7
- if (typeof value === "string" || typeof value === "number")
8
- return String(value);
9
- if (Array.isArray(value))
10
- return value.map(toClassName).filter(Boolean).join(" ");
11
- if (typeof value === "object") {
12
- return Object.entries(value)
13
- .filter(([, enabled]) => enabled)
14
- .map(([name]) => name)
15
- .join(" ");
16
- }
17
- return "";
18
- }
19
- export function A(props) {
20
- props = merge({ inactiveClass: "inactive", activeClass: "active" }, props);
21
- const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end");
22
- const to = useResolvedPath(() => props.href);
23
- const href = useHref(to);
24
- const location = useLocation();
25
- const isActive = createMemo(() => {
26
- const to_ = to();
27
- if (to_ === undefined)
28
- return [false, false];
29
- // trailing slashes are ignored so `/route` and `/route/` share active state
30
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
31
- const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
32
- return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
33
- });
34
- const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass]
35
- .filter(Boolean)
36
- .join(" "));
37
- return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} class={className()} link aria-current={isActive()[1] ? "page" : undefined}/>);
38
- }
39
- export function Navigate(props) {
40
- const navigate = useNavigate();
41
- const location = useLocation();
42
- const { href, state } = props;
43
- const path = typeof href === "function" ? href({ navigate, location }) : href;
44
- navigate(path, { replace: true, state });
45
- return null;
46
- }
@@ -1,9 +0,0 @@
1
- import type { JSX } from "@solidjs/web";
2
- import type { BaseRouterProps } from "./components.jsx";
3
- export declare function hashParser(str: string): string;
4
- export type HashRouterProps = BaseRouterProps & {
5
- actionBase?: string;
6
- explicitLinks?: boolean;
7
- preload?: boolean;
8
- };
9
- export declare function HashRouter(props: HashRouterProps): JSX.Element;
@@ -1,41 +0,0 @@
1
- import { setupNativeEvents } from "../data/events.js";
2
- import { createRouter, scrollToHash, bindEvent } from "./createRouter.js";
3
- import { createBeforeLeave, keepDepth, notifyIfNotBlocked, saveCurrentDepth } from "../lifecycle.js";
4
- export function hashParser(str) {
5
- const to = str.replace(/^.*?#/, "");
6
- // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
7
- // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
8
- // to the current path so we can handle these in-page anchors correctly.
9
- if (!to.startsWith("/")) {
10
- const [, path = "/"] = window.location.hash.split("#", 2);
11
- return `${path}#${to}`;
12
- }
13
- return to;
14
- }
15
- export function HashRouter(props) {
16
- const getSource = () => window.location.hash.slice(1);
17
- const beforeLeave = createBeforeLeave();
18
- return createRouter({
19
- get: getSource,
20
- set({ value, replace, scroll, state }) {
21
- if (replace) {
22
- window.history.replaceState(keepDepth(state), "", "#" + value);
23
- }
24
- else {
25
- window.history.pushState(state, "", "#" + value);
26
- }
27
- const hashIndex = value.indexOf("#");
28
- const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
29
- scrollToHash(hash, scroll);
30
- saveCurrentDepth();
31
- },
32
- init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => !beforeLeave.confirm(delta && delta < 0 ? delta : getSource()))),
33
- create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase }),
34
- utils: {
35
- go: delta => window.history.go(delta),
36
- renderPath: path => `#${path}`,
37
- parsePath: hashParser,
38
- beforeLeave
39
- }
40
- })(props);
41
- }
@@ -1,24 +0,0 @@
1
- import type { LocationChange } from "../types.js";
2
- import type { BaseRouterProps } from "./components.jsx";
3
- import type { JSX } from "@solidjs/web";
4
- export type MemoryHistory = {
5
- get: () => string;
6
- set: (change: LocationChange) => void;
7
- go: (delta: number) => void;
8
- listen: (listener: (value: string) => void) => () => void;
9
- };
10
- export declare function createMemoryHistory(): {
11
- get: () => string;
12
- set: ({ value, scroll, replace }: LocationChange) => void;
13
- back: () => void;
14
- forward: () => void;
15
- go: (n: number) => void;
16
- listen: (listener: (value: string) => void) => () => void;
17
- };
18
- export type MemoryRouterProps = BaseRouterProps & {
19
- history?: MemoryHistory;
20
- actionBase?: string;
21
- explicitLinks?: boolean;
22
- preload?: boolean;
23
- };
24
- export declare function MemoryRouter(props: MemoryRouterProps): JSX.Element;