@alxia/react-router 0.8.1 → 0.9.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 (56) hide show
  1. package/README.md +107 -8
  2. package/dist/chunks/index-zqa30gr7.js +139 -0
  3. package/dist/chunks/index-zqa30gr7.js.map +12 -0
  4. package/dist/pwa/index.d.ts +14 -0
  5. package/dist/pwa/index.d.ts.map +1 -0
  6. package/dist/pwa/index.js +165 -0
  7. package/dist/pwa/index.js.map +14 -0
  8. package/dist/pwa/model/caching.d.ts +46 -0
  9. package/dist/pwa/model/caching.d.ts.map +1 -0
  10. package/dist/pwa/model/clear-app-caches.d.ts +9 -0
  11. package/dist/pwa/model/clear-app-caches.d.ts.map +1 -0
  12. package/dist/pwa/model/manifest.d.ts +80 -0
  13. package/dist/pwa/model/manifest.d.ts.map +1 -0
  14. package/dist/pwa/model/precache-list.d.ts +40 -0
  15. package/dist/pwa/model/precache-list.d.ts.map +1 -0
  16. package/dist/pwa/model/registration.d.ts +39 -0
  17. package/dist/pwa/model/registration.d.ts.map +1 -0
  18. package/dist/pwa/model/start-service-worker.d.ts +24 -0
  19. package/dist/pwa/model/start-service-worker.d.ts.map +1 -0
  20. package/dist/pwa/model/storable.d.ts +8 -0
  21. package/dist/pwa/model/storable.d.ts.map +1 -0
  22. package/dist/pwa/model/update-notice.d.ts +38 -0
  23. package/dist/pwa/model/update-notice.d.ts.map +1 -0
  24. package/dist/pwa/model/worker-contract.d.ts +61 -0
  25. package/dist/pwa/model/worker-contract.d.ts.map +1 -0
  26. package/dist/pwa/react/index.d.ts +9 -0
  27. package/dist/pwa/react/index.d.ts.map +1 -0
  28. package/dist/pwa/react/index.js +75 -0
  29. package/dist/pwa/react/index.js.map +13 -0
  30. package/dist/pwa/react/use-online.d.ts +6 -0
  31. package/dist/pwa/react/use-online.d.ts.map +1 -0
  32. package/dist/pwa/react/use-service-worker.d.ts +33 -0
  33. package/dist/pwa/react/use-service-worker.d.ts.map +1 -0
  34. package/dist/pwa/react/use-update-notice.d.ts +14 -0
  35. package/dist/pwa/react/use-update-notice.d.ts.map +1 -0
  36. package/dist/pwa/server/index.d.ts +7 -0
  37. package/dist/pwa/server/index.d.ts.map +1 -0
  38. package/dist/pwa/server/index.js +18 -0
  39. package/dist/pwa/server/index.js.map +10 -0
  40. package/dist/pwa/server/no-cache.d.ts +22 -0
  41. package/dist/pwa/server/no-cache.d.ts.map +1 -0
  42. package/docs/README.md +12 -3
  43. package/docs/guide/beside-the-pages.md +178 -0
  44. package/docs/guide/csp-nonce.md +100 -0
  45. package/docs/guide/customising-the-server.md +292 -0
  46. package/docs/guide/deploying.md +99 -0
  47. package/docs/guide/getting-started.md +112 -0
  48. package/docs/guide/how-it-works.md +262 -0
  49. package/docs/guide/openapi.md +51 -0
  50. package/docs/guide/pwa.md +475 -0
  51. package/docs/guide/testing.md +42 -0
  52. package/docs/guide/typing-the-loaders.md +311 -0
  53. package/docs/guide.md +27 -1391
  54. package/docs/roadmap.md +25 -1
  55. package/docs/troubleshooting.md +194 -8
  56. package/package.json +26 -4
@@ -0,0 +1,311 @@
1
+ # Typing the loaders
2
+
3
+ This page covers reading alxia's context in a loader, an action or a
4
+ middleware, typed with `Register` or a type argument, and the keys an app
5
+ makes for itself.
6
+
7
+ Every request through the catch-all sets `alxiaContext`, this package's
8
+ key, on React Router's context provider, to what alxia's middlewares built for
9
+ that request. Three forms read it, each the same object:
10
+
11
+ - **`withAlxia(fn)`**, for a route typed by the generated `Route.*Args`:
12
+ `fn` gets `alxia` beside React Router's arguments;
13
+ - **`alxiaOf(context)`**, inside a plain function, and with a type argument;
14
+ - **`context.alxia`**, where the arguments are typed with `react-router`'s
15
+ own `LoaderFunctionArgs` and `ActionFunctionArgs`
16
+ ([below](#the-shorthand-contextalxia)).
17
+
18
+ ## `withAlxia(fn)`
19
+
20
+ ```ts
21
+ // app/routes/account.tsx
22
+ import { type AlxiaArgs, withAlxia } from '@alxia/react-router';
23
+ import { redirect } from 'react-router';
24
+ import type { Route } from './+types/account';
25
+
26
+ export const loader = withAlxia(({ alxia, params }: Route.LoaderArgs & AlxiaArgs) => {
27
+ if (alxia.user === null) throw redirect('/login');
28
+ return { name: alxia.user.name, tab: params.tab ?? 'profile' };
29
+ });
30
+ // The page's Route.ComponentProps['loaderData']: { name: string; tab: string }
31
+ ```
32
+
33
+ Annotate `fn`'s parameter as the generated arguments and `AlxiaArgs`:
34
+ `AlxiaArgs` is `{ alxia }`, typed by `Register`, and `AlxiaArgs<typeof server>`
35
+ names a server where nothing is registered. `withAlxia` returns a function of
36
+ React Router's arguments alone, with `fn`'s return type, so the generated
37
+ `loaderData` and `actionData`, and `useLoaderData<typeof loader>`, read what
38
+ `fn` returns. An action is written the same way; a middleware too, and its
39
+ `next` is passed on:
40
+
41
+ ```ts
42
+ import { type AlxiaArgs, withAlxia } from '@alxia/react-router';
43
+ import type { Route } from './+types/admin';
44
+
45
+ export const middleware: Route.MiddlewareFunction[] = [
46
+ withAlxia(async ({ alxia }: Parameters<Route.MiddlewareFunction>[0] & AlxiaArgs, next) => {
47
+ alxia.log.info('admin');
48
+ return next();
49
+ }),
50
+ ];
51
+ ```
52
+
53
+ Why a wrapper: `context.alxia` cannot be typed under the generated
54
+ arguments ([below](#the-shorthand-contextalxia)), and a wrapper that adds a
55
+ key is the one form TypeScript infers from the annotation alone. A type
56
+ argument, `withAlxia<Route.LoaderArgs>(fn)`, would fix the arguments but
57
+ stop TypeScript inferring `fn`'s return type, since it infers no type
58
+ argument once one is given: the page's `loaderData` would be lost. On a request that did not come
59
+ through alxia it throws what `alxiaOf` throws.
60
+
61
+ ## With `Register`: no type argument
62
+
63
+ The `declare module` block in `app/server.ts` names the server once:
64
+
65
+ ```ts
66
+ declare module '@alxia/react-router' {
67
+ interface Register {
68
+ server: typeof server;
69
+ }
70
+ }
71
+ ```
72
+
73
+ Every loader, action and middleware then reads it typed, with no import:
74
+
75
+ ```ts
76
+ // app/routes/account.tsx
77
+ import { alxiaOf } from '@alxia/react-router';
78
+ import { data, redirect } from 'react-router';
79
+ import type { Route } from './+types/account';
80
+
81
+ export function loader({ context }: Route.LoaderArgs) {
82
+ const { user, log, requestId } = alxiaOf(context);
83
+ if (user === null) throw redirect('/login');
84
+ log.info('account');
85
+ return { name: user.name, requestId };
86
+ }
87
+
88
+ export async function action({ request, context }: Route.ActionArgs) {
89
+ const { user } = alxiaOf(context);
90
+ const form = await request.formData();
91
+ if (user === null) return data({ error: 'signed out' }, { status: 401 });
92
+ return { saved: form.get('name'), by: user.name };
93
+ }
94
+ ```
95
+
96
+ Name `typeof server`, the default export's type, not the module's
97
+ (`typeof import('./server')`): a wrong registration makes every read a
98
+ compile error, as [the troubleshooting entry](../troubleshooting.md#property--does-not-exist-on-type-basecontext---readonly-registerserver-must-be-typeof-server-the-default-export-of-createserver-never-)
99
+ shows. The checks it gives:
100
+
101
+ ```ts
102
+ const ctx = alxiaOf(context);
103
+ ctx.tenant; // error: no middleware derives `tenant`
104
+ ctx.user.name; // error: `user` may be null
105
+ ```
106
+
107
+ ## Without `Register`: the type argument
108
+
109
+ Pass the server's type, imported with `import type`, so nothing is
110
+ bundled and the route never imports the server at runtime:
111
+
112
+ ```ts
113
+ // app/server.ts
114
+ const server = createServer({ configure: (app) => app.use(logger()) });
115
+ export default server;
116
+ export type Server = typeof server;
117
+ ```
118
+
119
+ ```ts
120
+ // app/routes/home.tsx
121
+ import { alxiaOf } from '@alxia/react-router';
122
+ import type { Server } from '../server';
123
+ import type { Route } from './+types/home';
124
+
125
+ export function loader({ context }: Route.LoaderArgs) {
126
+ const { log } = alxiaOf<Server>(context);
127
+ log.info('home');
128
+ return null;
129
+ }
130
+ ```
131
+
132
+ The type argument may also be an alxia app, the app *before* the
133
+ catch-all, for a server of your own. Anything else is a compile error.
134
+
135
+ ## With `@alxia/core`'s `Register`
136
+
137
+ `@alxia/core` has a `Register` of its own, which names the chain that
138
+ builds an app's context, for its route files
139
+ ([The app's type](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/types.md#register-and-appcontext)).
140
+ `alxiaOf(context)` reads it when this package's `Register` names no
141
+ server:
142
+
143
+ ```ts
144
+ // app/context.ts
145
+ import { alxia } from '@alxia/core';
146
+
147
+ export const base = alxia().derive(({ request }) => ({ user: request.headers.get('x-user') }));
148
+
149
+ declare module '@alxia/core' {
150
+ interface Register {
151
+ context: typeof base;
152
+ }
153
+ }
154
+
155
+ // app/server.ts
156
+ const server = createServer({ configure: (app) => app.plugin(base) });
157
+ export default server;
158
+ ```
159
+
160
+ `alxiaOf(context).user` is then typed with no declaration in
161
+ `@alxia/react-router`. Which one is read, in order:
162
+
163
+ 1. a type argument, `alxiaOf<typeof server>(context)`;
164
+ 2. this package's `Register`, `server: typeof server`: the whole app the
165
+ pages run behind, `base` and everything `configure` adds after it;
166
+ 3. `@alxia/core`'s `Register`, `context: typeof base`: only what `base`
167
+ builds;
168
+ 4. neither: `BaseContext`, the request, its URL, `reply` and the rest of
169
+ what every middleware reads.
170
+
171
+ Declare this package's `Register` when `configure` adds to the context
172
+ after the base, and the loaders read it.
173
+
174
+ ## Why a global augmentation is right here
175
+
176
+ alxia's core refuses a global augmentation of a context key: a plugin
177
+ that added `user` to every route, declared before it or after, would
178
+ type `user` on routes that run before the plugin. That is the lie "order
179
+ is meaning" forbids. Core's own `Register` names the base that builds
180
+ the context instead, and what reads it requires it. `Register` here does
181
+ something else: it names the **one** server of the React Router build,
182
+ at the point of its catch-all, which is exactly what every loader runs
183
+ behind. One build has one server entry, so there is no second app for a
184
+ module to be confused with.
185
+
186
+ If two React Router apps share one TypeScript program (one tsconfig over
187
+ both folders of a monorepo), their two declarations conflict, and `tsc`
188
+ says so (`Subsequent property declarations must have the same type`).
189
+ Give each app its own tsconfig, or use the type argument in both.
190
+
191
+ ## The server: `server`
192
+
193
+ `alxiaOf(context).server` is core's `ctx.server`, the `Bun.Server` serving
194
+ the request, `undefined` with none
195
+ ([Serving](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/serving.md#the-server-ctxserver)).
196
+ It is not `Register`'s `server`, which names what `createServer()` returned
197
+ for the types. From an action, its `publish` reaches the app's
198
+ [WebSocket](beside-the-pages.md#websockets) subscribers:
199
+
200
+ ```ts
201
+ // app/routes/todos.tsx
202
+ import { alxiaOf } from '@alxia/react-router';
203
+ import type { Route } from './+types/todos';
204
+
205
+ export async function action({ request, context }: Route.ActionArgs) {
206
+ const title = String((await request.formData()).get('title'));
207
+ // Bun's own publish: the bytes as given, not checked by a socket's `send` schema.
208
+ alxiaOf(context).server?.publish('todos', JSON.stringify({ added: title }));
209
+ return { added: title };
210
+ }
211
+ ```
212
+
213
+ What it is, by how the app runs:
214
+
215
+ | | `alxiaOf(context).server` |
216
+ | --- | --- |
217
+ | `bun build/server/index.js`, `server.start(app)`, `app.listen()` | the server `listen` started |
218
+ | `react-router dev`, `vite preview` | the `Bun.Server` on a loopback port that the plugin relays the app's sockets to (Vite's own server is `node:http`, not a `Bun.Server`): `publish` reaches every socket the app opened, its `url` is that port's, and `requestIP` and `timeout` know nothing of the request Vite took, so `ctx.ip` stays `undefined` there |
219
+ | Vite in middleware mode, a server of your own calling `app.fetch(request)` alone, `app.request(…)` in a test | `undefined` |
220
+
221
+ ## The shorthand: `context.alxia`
222
+
223
+ This package augments React Router's `RouterContextProvider` with a
224
+ read-only `alxia` property: the object `alxiaOf(context)` returns, with no
225
+ import and no call, typed by the same `Register` (core's, then
226
+ `BaseContext`, with none of this package's):
227
+
228
+ ```ts
229
+ import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
230
+
231
+ export function loader({ context }: LoaderFunctionArgs) {
232
+ const { user, server } = context.alxia;
233
+ return { name: user?.name ?? 'anonymous', listening: server !== undefined };
234
+ }
235
+
236
+ export async function action({ context }: ActionFunctionArgs) {
237
+ context.alxia.server?.publish('todos', JSON.stringify({ changed: true }));
238
+ return { ok: true };
239
+ }
240
+ ```
241
+
242
+ **Typed with react-router's own argument types, not yet the generated
243
+ ones.** `LoaderFunctionArgs`, `ActionFunctionArgs`, `MiddlewareFunction` and
244
+ `RouterContextProvider`, imported from `react-router`, see `alxia`. The
245
+ generated `Route.LoaderArgs` does not: the `+types` files import from
246
+ `react-router/internal`, whose types (in react-router 8.4 and 8.5) point at its
247
+ `dist/development` build, while `react-router` itself points at
248
+ `dist/production`. That is a second declaration of `RouterContextProvider`,
249
+ which no exported path reaches, so no package can augment it. With
250
+ `Route.LoaderArgs`, `context.alxia` works at runtime and `tsc` reports
251
+ TS2339 ([Troubleshooting](../troubleshooting.md#property-alxia-does-not-exist-on-type-readonlyroutercontextprovider)):
252
+ use `withAlxia` (or `alxiaOf(context)`) there, until react-router ships one declaration.
253
+
254
+ It is a getter on the provider's prototype that reads `alxiaContext`: it
255
+ holds exactly when that key is set, in dev as in production, and on a
256
+ provider no catch-all filled it throws what `alxiaOf` throws. It is the
257
+ server's alone: a `clientLoader` or a `clientAction` has no alxia context.
258
+
259
+ ## Outside the catch-all
260
+
261
+ Without the plugin, under a plain `react-router dev`, or in a unit test
262
+ that calls a loader with a bare provider, there is no alxia context, and
263
+ `alxiaOf` and `context.alxia` throw, saying so. The getter is installed
264
+ when `@alxia/react-router` is first imported: a test whose program never
265
+ imports it reads `context.alxia` as `undefined`. See
266
+ [the troubleshooting entry](../troubleshooting.md#alxiaof-this-request-has-no-alxia-context-).
267
+
268
+ ## The app's own context keys
269
+
270
+ React Router 8 passes data to loaders through keys made by
271
+ `createContext<T>()`, and matches a key by object identity. Under the
272
+ plugin, the server is built and loaded with the routes, so a key in
273
+ `app/` is one object for both:
274
+
275
+ ```ts
276
+ // app/context.ts
277
+ import { createContext } from 'react-router';
278
+
279
+ export const greetingContext = createContext<string>('unset');
280
+ ```
281
+
282
+ ```ts
283
+ // app/server.ts
284
+ import { createServer } from '@alxia/react-router';
285
+ import { greetingContext } from './context';
286
+
287
+ export default createServer({
288
+ getLoadContext: (_ctx, context) => context.set(greetingContext, 'hello'),
289
+ });
290
+ ```
291
+
292
+ ```ts
293
+ // app/routes/home.tsx
294
+ import { greetingContext } from '../context';
295
+ import type { Route } from './+types/home';
296
+
297
+ export function loader({ context }: Route.LoaderArgs) {
298
+ return { greeting: context.get(greetingContext) };
299
+ }
300
+ ```
301
+
302
+ `getLoadContext` runs before React Router, on every request, with
303
+ `alxiaContext` already set. Its `ctx` is typed by `configure`'s app:
304
+ reading something no middleware derives is a compile error. Keep the keys in a
305
+ module of their own, such as `app/context.ts`, rather than in
306
+ `app/server.ts`: a route imports them, and the server file then stays out
307
+ of the client's module graph.
308
+
309
+ A server of your own, without the plugin, holds another copy of every
310
+ key under `app/`: see
311
+ [the troubleshooting entry](../troubleshooting.md#error-no-value-found-for-context).