@solidjs/router 1.0.0-next.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +715 -531
  2. package/dist/components.d.ts +31 -0
  3. package/dist/components.jsx +40 -0
  4. package/dist/data/action.d.ts +11 -40
  5. package/dist/data/action.js +100 -293
  6. package/dist/data/createAsync.d.ts +32 -0
  7. package/dist/data/createAsync.js +96 -0
  8. package/dist/data/events.d.ts +0 -8
  9. package/dist/data/events.js +23 -24
  10. package/dist/data/index.d.ts +4 -2
  11. package/dist/data/index.js +4 -2
  12. package/dist/data/query.d.ts +3 -1
  13. package/dist/data/query.js +22 -39
  14. package/dist/data/response.d.ts +4 -0
  15. package/dist/data/response.js +42 -0
  16. package/dist/index.d.ts +3 -5
  17. package/dist/index.js +1024 -1597
  18. package/dist/index.jsx +2 -2
  19. package/dist/lifecycle.d.ts +4 -29
  20. package/dist/lifecycle.js +37 -40
  21. package/dist/routers/HashRouter.d.ts +9 -0
  22. package/dist/routers/HashRouter.js +41 -0
  23. package/dist/routers/MemoryRouter.d.ts +24 -0
  24. package/dist/routers/MemoryRouter.js +57 -0
  25. package/dist/routers/Router.d.ts +17 -0
  26. package/dist/routers/Router.js +59 -0
  27. package/dist/routers/StaticRouter.d.ts +6 -0
  28. package/dist/routers/StaticRouter.js +15 -0
  29. package/dist/routers/components.d.ts +26 -12
  30. package/dist/routers/components.jsx +54 -68
  31. package/dist/routers/createRouter.d.ts +10 -0
  32. package/dist/routers/createRouter.js +41 -0
  33. package/dist/routers/index.d.ts +11 -4
  34. package/dist/routers/index.js +6 -2
  35. package/dist/routers/scrollRestoration.d.ts +32 -0
  36. package/dist/routers/scrollRestoration.js +96 -0
  37. package/dist/routing.d.ts +52 -88
  38. package/dist/routing.js +166 -381
  39. package/dist/types.d.ts +33 -83
  40. package/dist/utils.d.ts +0 -2
  41. package/dist/utils.js +0 -2
  42. package/package.json +7 -11
  43. package/dist/claims.d.ts +0 -21
  44. package/dist/claims.js +0 -115
  45. package/dist/data/flash.d.ts +0 -21
  46. package/dist/data/flash.js +0 -78
  47. package/dist/data/flashCookie.d.ts +0 -7
  48. package/dist/data/flashCookie.js +0 -20
  49. package/dist/data/serverForms.d.ts +0 -1
  50. package/dist/data/serverForms.js +0 -5
  51. package/dist/paths.d.ts +0 -117
  52. package/dist/paths.js +0 -41
  53. package/dist/routers/factory.d.ts +0 -45
  54. package/dist/routers/factory.jsx +0 -143
  55. package/dist/routers/history.d.ts +0 -24
  56. package/dist/routers/history.js +0 -180
  57. package/dist/server.d.ts +0 -75
  58. package/dist/server.js +0 -264
package/dist/types.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { Component, Signal } from "solid-js";
2
- import type { JSX, ResponseEnvelope } from "@solidjs/web";
3
- declare module "@solidjs/web" {
1
+ import type { Component, JSX, Signal } from "solid-js";
2
+ declare module "solid-js/web" {
4
3
  interface RequestEvent {
5
4
  response: {
6
5
  status?: number;
@@ -12,8 +11,7 @@ declare module "@solidjs/web" {
12
11
  cache?: Map<string, CacheEntry>;
13
12
  submission?: {
14
13
  input: any;
15
- result?: any;
16
- error?: any;
14
+ result: any;
17
15
  url: string;
18
16
  };
19
17
  dataOnly?: boolean | string[];
@@ -25,42 +23,6 @@ declare module "@solidjs/web" {
25
23
  }
26
24
  export type Params = Record<string, string | undefined>;
27
25
  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
- };
64
26
  export type SetParams = Record<string, string | number | boolean | null | undefined>;
65
27
  export type SetSearchParams = Record<string, string | string[] | number | number[] | boolean | boolean[] | null | undefined>;
66
28
  export interface Path {
@@ -80,7 +42,7 @@ export interface NavigateOptions<S = unknown> {
80
42
  state: S;
81
43
  }
82
44
  export interface Navigator {
83
- (to: string | TypedPath | number, options?: Partial<NavigateOptions>): void;
45
+ (to: string | number, options?: Partial<NavigateOptions>): void;
84
46
  (delta: number): void;
85
47
  }
86
48
  export type NavigatorFactory = (route?: RouteContext) => Navigator;
@@ -92,7 +54,8 @@ export interface LocationChange<S = unknown> {
92
54
  rawPath?: string;
93
55
  }
94
56
  export interface RouterIntegration {
95
- signal: [get: () => LocationChange, set: (next: LocationChange) => void];
57
+ signal: Signal<LocationChange>;
58
+ create?: (router: RouterContext) => void;
96
59
  utils?: Partial<RouterUtils>;
97
60
  }
98
61
  export type Intent = "initial" | "native" | "navigate" | "preload";
@@ -109,29 +72,15 @@ export interface RouteSectionProps<T = unknown> {
109
72
  children?: JSX.Element;
110
73
  }
111
74
  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
- }>;
126
75
  export type RouteDefinition<S extends string | string[] = any, T = any> = {
127
76
  path?: S;
128
77
  matchFilters?: MatchFilters<S>;
129
78
  preload?: RoutePreloadFunc<T>;
130
- children?: RouteDefinition | readonly RouteDefinition[] | LazyRouteChildren;
79
+ children?: RouteDefinition | RouteDefinition[];
131
80
  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>;
134
81
  info?: Record<string, any>;
82
+ /** @deprecated use preload */
83
+ load?: RoutePreloadFunc;
135
84
  };
136
85
  export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
137
86
  export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : [];
@@ -161,14 +110,6 @@ export interface RouteDescription {
161
110
  matcher: (location: string) => PathMatch | null;
162
111
  matchFilters?: MatchFilters;
163
112
  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[];
172
113
  }
173
114
  export interface Branch {
174
115
  routes: RouteDescription[];
@@ -179,7 +120,6 @@ export interface RouteContext {
179
120
  parent?: RouteContext;
180
121
  child?: RouteContext;
181
122
  pattern: string;
182
- params: Params;
183
123
  path: () => string;
184
124
  outlet: () => JSX.Element;
185
125
  resolvePath(to: string): string | undefined;
@@ -188,7 +128,7 @@ export interface RouterUtils {
188
128
  renderPath(path: string): string;
189
129
  parsePath(str: string): string;
190
130
  go(delta: number): void;
191
- beforeLeave: BeforeLeaveSlot;
131
+ beforeLeave: BeforeLeaveLifecycle;
192
132
  paramsWrapper: (getParams: () => Params, branches: () => Branch[]) => Params;
193
133
  queryWrapper: (getQuery: () => SearchParams) => SearchParams;
194
134
  }
@@ -196,7 +136,6 @@ export interface RouterContext {
196
136
  base: RouteContext;
197
137
  location: Location;
198
138
  params: Params;
199
- wrapParams: (getParams: () => Params) => Params;
200
139
  navigatorFactory: NavigatorFactory;
201
140
  isRouting: () => boolean;
202
141
  /** The target of the in-flight navigation transition, if any. Not reactive. */
@@ -204,7 +143,7 @@ export interface RouterContext {
204
143
  matches: () => RouteMatch[];
205
144
  renderPath(path: string): string;
206
145
  parsePath(str: string): string;
207
- beforeLeave: BeforeLeaveSlot;
146
+ beforeLeave: BeforeLeaveLifecycle;
208
147
  preloadRoute: (url: URL, preloadData?: boolean) => void;
209
148
  singleFlight: boolean;
210
149
  submissions: Signal<Submission<any, any>[]>;
@@ -226,28 +165,39 @@ export interface BeforeLeaveLifecycle {
226
165
  subscribe(listener: BeforeLeaveListener): () => void;
227
166
  confirm(to: string | number, options?: Partial<NavigateOptions>): boolean;
228
167
  }
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
- }
238
168
  export type Submission<T, U> = {
239
169
  readonly input: T;
240
170
  readonly result?: U;
241
171
  readonly error: any;
172
+ readonly pending: boolean;
242
173
  readonly url: string;
243
174
  clear: () => void;
244
175
  retry: () => void;
245
176
  };
177
+ export type SubmissionStub = {
178
+ readonly input: undefined;
179
+ readonly result: undefined;
180
+ readonly error: undefined;
181
+ readonly pending: undefined;
182
+ readonly url: undefined;
183
+ clear: () => void;
184
+ retry: () => void;
185
+ };
246
186
  export interface MaybePreloadableComponent extends Component {
247
187
  preload?: () => void;
248
188
  }
249
189
  export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
250
190
  count: number;
251
191
  }];
252
- export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
253
- export {};
192
+ export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
193
+ export type RouterResponseInit = Omit<ResponseInit, "body"> & {
194
+ revalidate?: string | string[];
195
+ };
196
+ export type CustomResponse<T> = Omit<Response, "clone"> & {
197
+ customBody: () => T;
198
+ clone(...args: readonly unknown[]): CustomResponse<T>;
199
+ };
200
+ /** @deprecated */
201
+ export type RouteLoadFunc = RoutePreloadFunc;
202
+ /** @deprecated */
203
+ export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,6 @@
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;
6
4
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
7
5
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
8
6
  export declare function joinPaths(from: string, to: string): string;
package/dist/utils.js CHANGED
@@ -6,8 +6,6 @@ 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(/\/$/, "");
11
9
  export function resolvePath(base, path, from) {
12
10
  if (hasSchemeRegex.test(path)) {
13
11
  return undefined;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "1.0.0-next.9",
9
+ "version": "1.0.0",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -22,15 +22,13 @@
22
22
  ".": {
23
23
  "solid": "./dist/index.jsx",
24
24
  "default": "./dist/index.js"
25
- },
26
- "./server": "./dist/server.js"
25
+ }
27
26
  },
28
27
  "files": [
29
28
  "dist"
30
29
  ],
31
30
  "sideEffects": false,
32
31
  "devDependencies": {
33
- "@solidjs/web": "2.0.0-beta.22",
34
32
  "@babel/core": "^7.26.0",
35
33
  "@babel/preset-typescript": "^7.26.0",
36
34
  "@changesets/cli": "^2.27.10",
@@ -39,25 +37,23 @@
39
37
  "@rollup/plugin-terser": "0.4.4",
40
38
  "@types/jest": "^29.5.14",
41
39
  "@types/node": "^22.10.0",
42
- "babel-preset-solid": "2.0.0-beta.22",
40
+ "babel-preset-solid": "^1.9.3",
43
41
  "jsdom": "^25.0.1",
44
42
  "prettier": "^3.4.1",
45
43
  "rollup": "^4.27.4",
46
- "solid-js": "2.0.0-beta.22",
44
+ "solid-js": "^1.9.3",
47
45
  "typescript": "^5.7.2",
48
46
  "vite": "^6.0.0",
49
- "vite-plugin-solid": "3.0.0-next.10",
47
+ "vite-plugin-solid": "^2.11.0",
50
48
  "vitest": "^2.1.6"
51
49
  },
52
50
  "peerDependencies": {
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"
51
+ "solid-js": "^1.8.6"
55
52
  },
56
53
  "scripts": {
57
54
  "build": "rm -rf dist && tsc && rollup -c",
58
- "test": "vitest run && vitest run --config vitest.config.server.ts && npm run test:types",
55
+ "test": "vitest run && npm run test:types",
59
56
  "test:watch": "vitest",
60
- "test:server": "vitest run --config vitest.config.server.ts",
61
57
  "test:types": "tsc --project tsconfig.test.json",
62
58
  "pretty": "prettier --write \"{src,test}/**/*.{ts,tsx}\"",
63
59
  "release": "pnpm build && changeset publish"
package/dist/claims.d.ts DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,115 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
1
- export { FLASH_COOKIE, hasFlashCookie, clearFlashCookie } from "./flashCookie.js";
2
- /** What rides the cookie: a Submission minus its lifecycle methods. */
3
- export interface FlashSubmission {
4
- input: any[];
5
- url: string;
6
- result?: any;
7
- error?: any;
8
- }
9
- /**
10
- * Encodes the outcome of a no-JS submission as a Set-Cookie value. `url` is
11
- * the action url (pathname + search of the server function call) so the
12
- * seeded submission matches `useSubmission` filters; `thrown` errors land on
13
- * `error`, returned values on `result` (mirroring the scripted split).
14
- */
15
- export declare function encodeFlashCookie(url: string, result: any, input: any[], thrown?: boolean): string;
16
- /**
17
- * Decodes the flash cookie out of a request's Cookie header. Returns
18
- * undefined when absent or unreadable (a malformed cookie must never take
19
- * down SSR — it is cleared either way).
20
- */
21
- export declare function decodeFlashCookie(cookieHeader: string | null): FlashSubmission | undefined;
@@ -1,78 +0,0 @@
1
- // The no-JS form convention's cookie codec. When a form posts to a server
2
- // function without the client runtime (no instance header), the server
3
- // handler redirects back carrying the outcome in a one-shot "flash" cookie;
4
- // the next SSR pass reads it and seeds the router's submission state so
5
- // useSubmission() renders the result exactly as a scripted submission would.
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";
13
- // Form payloads have no JSON encoding, so entries are captured as pair
14
- // arrays under a marker key ($f / $u) and revived to real FormData /
15
- // URLSearchParams on the way out. File entries cannot ride a cookie and are
16
- // dropped.
17
- function encodeInputValue(value) {
18
- if (value instanceof FormData)
19
- return { $f: [...value.entries()].filter(([, v]) => typeof v === "string") };
20
- if (value instanceof URLSearchParams)
21
- return { $u: [...value.entries()] };
22
- return value;
23
- }
24
- function decodeInputValue(value) {
25
- if (value && typeof value === "object") {
26
- if (Array.isArray(value.$f)) {
27
- const form = new FormData();
28
- for (const [k, v] of value.$f)
29
- form.append(k, v);
30
- return form;
31
- }
32
- if (Array.isArray(value.$u))
33
- return new URLSearchParams(value.$u);
34
- }
35
- return value;
36
- }
37
- /**
38
- * Encodes the outcome of a no-JS submission as a Set-Cookie value. `url` is
39
- * the action url (pathname + search of the server function call) so the
40
- * seeded submission matches `useSubmission` filters; `thrown` errors land on
41
- * `error`, returned values on `result` (mirroring the scripted split).
42
- */
43
- export function encodeFlashCookie(url, result, input, thrown) {
44
- const isError = result instanceof Error;
45
- const payload = {
46
- url,
47
- result: isError ? result.message : result,
48
- error: isError,
49
- thrown: !!thrown,
50
- input: input.map(encodeInputValue)
51
- };
52
- return `${FLASH_COOKIE}=${encodeURIComponent(JSON.stringify(payload))}; Secure; HttpOnly; Path=/`;
53
- }
54
- /**
55
- * Decodes the flash cookie out of a request's Cookie header. Returns
56
- * undefined when absent or unreadable (a malformed cookie must never take
57
- * down SSR — it is cleared either way).
58
- */
59
- export function decodeFlashCookie(cookieHeader) {
60
- const match = matchFlashCookie(cookieHeader);
61
- if (!match)
62
- return;
63
- try {
64
- const payload = JSON.parse(decodeURIComponent(match));
65
- if (!payload || !payload.result)
66
- return;
67
- const result = payload.error ? new Error(payload.result) : payload.result;
68
- return {
69
- input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
70
- url: payload.url,
71
- result: payload.thrown ? undefined : result,
72
- error: payload.thrown ? result : undefined
73
- };
74
- }
75
- catch (error) {
76
- console.error(error);
77
- }
78
- }
@@ -1,7 +0,0 @@
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;
@@ -1,20 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export { submitServerForm } from "./action.js";
@@ -1,5 +0,0 @@
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";