@alxia/react-router 0.8.1 → 0.10.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.
- package/README.md +222 -10
- package/dist/chunks/index-59vhfj6n.js +10 -0
- package/dist/chunks/index-59vhfj6n.js.map +10 -0
- package/dist/chunks/index-5gqcpyyg.js +34 -0
- package/dist/chunks/index-5gqcpyyg.js.map +10 -0
- package/dist/chunks/index-9xv2a8et.js +108 -0
- package/dist/chunks/index-9xv2a8et.js.map +11 -0
- package/dist/chunks/index-mppz0806.js +119 -0
- package/dist/chunks/index-mppz0806.js.map +11 -0
- package/dist/chunks/index-rfr02vg4.js +40 -0
- package/dist/chunks/index-rfr02vg4.js.map +10 -0
- package/dist/chunks/index-zdbqzcjp.js +97 -0
- package/dist/chunks/index-zdbqzcjp.js.map +12 -0
- package/dist/pwa/build/build-worker.d.ts +41 -0
- package/dist/pwa/build/build-worker.d.ts.map +1 -0
- package/dist/pwa/build/index.d.ts +9 -0
- package/dist/pwa/build/index.d.ts.map +1 -0
- package/dist/pwa/build/index.js +9 -0
- package/dist/pwa/build/index.js.map +9 -0
- package/dist/pwa/build/precache.d.ts +17 -0
- package/dist/pwa/build/precache.d.ts.map +1 -0
- package/dist/pwa/index.d.ts +14 -0
- package/dist/pwa/index.d.ts.map +1 -0
- package/dist/pwa/index.js +65 -0
- package/dist/pwa/index.js.map +10 -0
- package/dist/pwa/model/caching.d.ts +46 -0
- package/dist/pwa/model/caching.d.ts.map +1 -0
- package/dist/pwa/model/clear-app-caches.d.ts +9 -0
- package/dist/pwa/model/clear-app-caches.d.ts.map +1 -0
- package/dist/pwa/model/manifest.d.ts +80 -0
- package/dist/pwa/model/manifest.d.ts.map +1 -0
- package/dist/pwa/model/precache-list.d.ts +40 -0
- package/dist/pwa/model/precache-list.d.ts.map +1 -0
- package/dist/pwa/model/registration.d.ts +39 -0
- package/dist/pwa/model/registration.d.ts.map +1 -0
- package/dist/pwa/model/start-service-worker.d.ts +24 -0
- package/dist/pwa/model/start-service-worker.d.ts.map +1 -0
- package/dist/pwa/model/storable.d.ts +8 -0
- package/dist/pwa/model/storable.d.ts.map +1 -0
- package/dist/pwa/model/update-notice.d.ts +38 -0
- package/dist/pwa/model/update-notice.d.ts.map +1 -0
- package/dist/pwa/model/worker-contract.d.ts +61 -0
- package/dist/pwa/model/worker-contract.d.ts.map +1 -0
- package/dist/pwa/react/index.d.ts +9 -0
- package/dist/pwa/react/index.d.ts.map +1 -0
- package/dist/pwa/react/index.js +75 -0
- package/dist/pwa/react/index.js.map +13 -0
- package/dist/pwa/react/use-online.d.ts +6 -0
- package/dist/pwa/react/use-online.d.ts.map +1 -0
- package/dist/pwa/react/use-service-worker.d.ts +33 -0
- package/dist/pwa/react/use-service-worker.d.ts.map +1 -0
- package/dist/pwa/react/use-update-notice.d.ts +14 -0
- package/dist/pwa/react/use-update-notice.d.ts.map +1 -0
- package/dist/pwa/server/index.d.ts +7 -0
- package/dist/pwa/server/index.d.ts.map +1 -0
- package/dist/pwa/server/index.js +18 -0
- package/dist/pwa/server/index.js.map +10 -0
- package/dist/pwa/server/no-cache.d.ts +22 -0
- package/dist/pwa/server/no-cache.d.ts.map +1 -0
- package/dist/pwa/worker/index.d.ts +9 -0
- package/dist/pwa/worker/index.d.ts.map +1 -0
- package/dist/pwa/worker/index.js +144 -0
- package/dist/pwa/worker/index.js.map +12 -0
- package/dist/pwa/worker/injected.d.ts +15 -0
- package/dist/pwa/worker/injected.d.ts.map +1 -0
- package/dist/pwa/worker/plan.d.ts +65 -0
- package/dist/pwa/worker/plan.d.ts.map +1 -0
- package/dist/pwa/worker/scope.d.ts +18 -0
- package/dist/pwa/worker/scope.d.ts.map +1 -0
- package/dist/pwa/worker/start-worker.d.ts +17 -0
- package/dist/pwa/worker/start-worker.d.ts.map +1 -0
- package/dist/vite/index.d.ts +10 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +49 -3
- package/dist/vite/index.js.map +6 -5
- package/dist/vite/runtime.d.ts +1 -1
- package/dist/vite/runtime.d.ts.map +1 -1
- package/dist/vite/worker.d.ts +16 -0
- package/dist/vite/worker.d.ts.map +1 -0
- package/docs/README.md +12 -3
- package/docs/guide/beside-the-pages.md +178 -0
- package/docs/guide/csp-nonce.md +100 -0
- package/docs/guide/customising-the-server.md +292 -0
- package/docs/guide/deploying.md +99 -0
- package/docs/guide/getting-started.md +112 -0
- package/docs/guide/how-it-works.md +268 -0
- package/docs/guide/openapi.md +51 -0
- package/docs/guide/pwa.md +636 -0
- package/docs/guide/testing.md +42 -0
- package/docs/guide/typing-the-loaders.md +311 -0
- package/docs/guide.md +27 -1391
- package/docs/roadmap.md +39 -1
- package/docs/troubleshooting.md +402 -11
- package/package.json +70 -6
|
@@ -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).
|