@alxia/react-router 0.34.0 → 0.35.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 +42 -1
- package/dist/health.d.ts +21 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +5 -4
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -1
- package/docs/guide/customising-the-server.md +25 -0
- package/docs/guide/deploying.md +5 -2
- package/docs/roadmap.md +7 -0
- package/docs/troubleshooting.md +43 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -152,6 +152,47 @@ A request runs through `beforeAll`, then the client's files, then
|
|
|
152
152
|
`configure`, then the pages. So a guard in `configure` does not block the
|
|
153
153
|
login page's JavaScript.
|
|
154
154
|
|
|
155
|
+
### A health probe
|
|
156
|
+
|
|
157
|
+
`health` declares a probe ahead of everything: before `beforeAll`, the
|
|
158
|
+
client's files and every middleware, so it needs no session and sets no
|
|
159
|
+
cookie. It answers `200` `ok` as `text/plain`, with `cache-control: no-store`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// app/server.ts
|
|
163
|
+
import { createServer } from '@alxia/react-router';
|
|
164
|
+
|
|
165
|
+
// Stand-in for your dependency's ping.
|
|
166
|
+
const db = { ping: async () => true };
|
|
167
|
+
|
|
168
|
+
export default createServer({
|
|
169
|
+
// 503 `unavailable` when the check returns false, throws, rejects or
|
|
170
|
+
// outlasts `timeout` (1 000 ms by default). `undefined` counts as healthy.
|
|
171
|
+
health: { path: '/health', check: () => db.ping(), timeout: 500 },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`health: '/health'` alone is the same without a check. The probe is
|
|
176
|
+
liveness only, not a `/ready`: it is `@alxia/core`'s `health()` mounted with
|
|
177
|
+
`format: 'text'` and `ready: false`, so for readiness checks and the JSON
|
|
178
|
+
report mount `health()` itself on another `path`, as below. Being core's
|
|
179
|
+
probe, it is left out by `@alxia/openapi`'s `matchesSpec`
|
|
180
|
+
(`isHealthRoute`).
|
|
181
|
+
|
|
182
|
+
To add `/ready` beside the probe, mount core's `health()` in `beforeAll` on
|
|
183
|
+
another path, since `health()` also declares `/health` by default:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { health } from '@alxia/core';
|
|
187
|
+
import { createServer } from '@alxia/react-router';
|
|
188
|
+
|
|
189
|
+
export default createServer({
|
|
190
|
+
health: '/health', // text liveness, first of all
|
|
191
|
+
beforeAll: (app) =>
|
|
192
|
+
app.plugin(health({ path: '/live', checks: { database: () => true } })),
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
155
196
|
## Reading the context in a loader
|
|
156
197
|
|
|
157
198
|
What alxia's middlewares built for a request reaches a loader, an action or
|
|
@@ -1172,7 +1213,7 @@ export function loader({ request }: Route.LoaderArgs) {
|
|
|
1172
1213
|
|
|
1173
1214
|
| export | |
|
|
1174
1215
|
| --- | --- |
|
|
1175
|
-
| `createServer(options?)` | the server of `app/server.ts`. `beforeAll`, `configure`, `getLoadContext`, `proxy` (`alxia({ proxy })`'s, `trustProxy(…)` from `@alxia/core`), `build`, `mode`, `client`, `precompressed`, `listen`, `onListen` |
|
|
1216
|
+
| `createServer(options?)`, `ServerHealth` | the server of `app/server.ts`. `health` (a path or `{ path, check, timeout }`: a liveness probe declared first, in text), `beforeAll`, `configure`, `getLoadContext`, `proxy` (`alxia({ proxy })`'s, `trustProxy(…)` from `@alxia/core`), `build`, `mode`, `client`, `precompressed`, `listen`, `onListen` |
|
|
1176
1217
|
| `ServerOptions<Before, App>` | its options |
|
|
1177
1218
|
| `ReactRouterServer<App>` | what it returns: `create(wiring)` makes the app, `start(app)` listens |
|
|
1178
1219
|
| `ServerWiring` | what `create` takes: `build`, `mode`, `client` |
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createServer({ health })`: the liveness probe, declared first. It is
|
|
3
|
+
* `@alxia/core`'s `health()` mounted alone, in text.
|
|
4
|
+
*/
|
|
5
|
+
import { type AnyAlxia, type MaybePromise } from '@alxia/core';
|
|
6
|
+
/** `createServer({ health })` in its long form. */
|
|
7
|
+
export interface ServerHealth {
|
|
8
|
+
/** Where the probe answers, `GET`: `/health` by default. */
|
|
9
|
+
readonly path?: `/${string}`;
|
|
10
|
+
/**
|
|
11
|
+
* Called on each probe: the probe answers 503 `unavailable` when it
|
|
12
|
+
* returns `false`, throws, rejects or outlasts `timeout`; `undefined` or
|
|
13
|
+
* no return value counts as healthy. The error is not logged.
|
|
14
|
+
*/
|
|
15
|
+
readonly check?: () => MaybePromise<boolean | undefined | void>;
|
|
16
|
+
/** How long `check` may take, in milliseconds: 1 000 by default. */
|
|
17
|
+
readonly timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Declares the probe on `app`, before anything else. */
|
|
20
|
+
export declare function declareHealth(app: AnyAlxia, option: `/${string}` | ServerHealth): void;
|
|
21
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,QAAQ,EAAU,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEvE,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,YAAY,CAAC,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC;IAChE,oEAAoE;IACpE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAC5B,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,IAAI,MAAM,EAAE,GAAG,YAAY,GACjC,IAAI,CAYN"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { type AlxiaContextOf, type AppOf, alxiaContext, alxiaOf, type InvalidRegister, type ProviderLike, type Register, type RegisteredApp, type RegisteredOf, } from './context';
|
|
2
|
+
export type { ServerHealth } from './health';
|
|
2
3
|
export { nonceOf } from './nonce';
|
|
3
4
|
export { isReactRouterRoute, type ReactRouterOptions, reactRouter, } from './react-router';
|
|
4
5
|
export { createServer, type FreshApp, type ReactRouterServer, type ServerOptions, type ServerWiring, } from './server';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,KAAK,EACV,YAAY,EACZ,OAAO,EACP,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,KAAK,EACV,YAAY,EACZ,OAAO,EACP,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,21 @@ function pathOf(client) {
|
|
|
117
117
|
import {
|
|
118
118
|
alxia
|
|
119
119
|
} from "@alxia/core";
|
|
120
|
+
|
|
121
|
+
// src/health.ts
|
|
122
|
+
import { health } from "@alxia/core";
|
|
123
|
+
function declareHealth(app, option) {
|
|
124
|
+
const { path, check, timeout } = typeof option === "string" ? { path: option } : option;
|
|
125
|
+
app.plugin(health({
|
|
126
|
+
format: "text",
|
|
127
|
+
ready: false,
|
|
128
|
+
...path === undefined ? {} : { path },
|
|
129
|
+
...check === undefined ? {} : { live: check },
|
|
130
|
+
...timeout === undefined ? {} : { timeout }
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/server.ts
|
|
120
135
|
function createServer(options = {}) {
|
|
121
136
|
return {
|
|
122
137
|
create(wiring) {
|
|
@@ -126,6 +141,8 @@ function createServer(options = {}) {
|
|
|
126
141
|
dev: mode === "development",
|
|
127
142
|
...options.proxy === undefined ? {} : { proxy: options.proxy }
|
|
128
143
|
});
|
|
144
|
+
if (options.health !== undefined)
|
|
145
|
+
declareHealth(fresh, options.health);
|
|
129
146
|
const before = options.beforeAll?.(fresh) ?? fresh;
|
|
130
147
|
if (client !== undefined && mode === "production") {
|
|
131
148
|
declareClient(before, client, options.precompressed ?? true);
|
|
@@ -176,5 +193,5 @@ export {
|
|
|
176
193
|
withAlxia
|
|
177
194
|
};
|
|
178
195
|
|
|
179
|
-
//# debugId=
|
|
196
|
+
//# debugId=BACF49A4D19535D964756E2164756E21
|
|
180
197
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/react-router.ts", "../src/assets.ts", "../src/server.ts", "../src/with-alxia.ts"],
|
|
3
|
+
"sources": ["../src/react-router.ts", "../src/assets.ts", "../src/server.ts", "../src/health.ts", "../src/with-alxia.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { fileURLToPath } from 'node:url';\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\ttype BaseContext,\n\ttype MaybePromise,\n\toriginalUrl,\n\ttype RouteDefinition,\n\ttype StatusCode,\n} from '@alxia/core';\nimport {\n\tcreateRequestHandler,\n\ttype RequestHandler,\n\tRouterContextProvider,\n\ttype ServerBuild,\n} from 'react-router';\nimport { serveClient } from './assets';\nimport { alxiaContext } from './context';\n\nexport interface ReactRouterOptions<Ctx> {\n\t/**\n\t * React Router's server build: the module itself, or a function that\n\t * returns it — `() => import('virtual:react-router/server-build')` in a\n\t * server entry Vite builds, `() => import('./build/server/index.js')`\n\t * beside a build. A function is called on every request in\n\t * `development`, so an edit is picked up, and once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** React Router's server mode: `production` by default. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Sets the app's own keys on React Router's context provider, from the\n\t * context alxia's middlewares built. `ctx` is typed by the app at the point of\n\t * `app.plugin`: reading what nothing before it adds is a compile error.\n\t * `alxiaContext` is always set, whether or not this is given.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: BaseContext & Ctx,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The client build's folder, `build/client`: a path or a `file:` URL.\n\t * Its `assets/` is served immutable, and every other top-level file or\n\t * folder — the copies of `public/` — with an hour's cache, before the\n\t * catch-all. Ignored in `development`, where Vite serves them.\n\t */\n\treadonly client?: string | URL;\n\t/**\n\t * Serves the `.br` and `.gz` copies `alxia()` writes beside the client\n\t * build's files to a browser that accepts them, with `Vary:\n\t * Accept-Encoding`: `true` by default. `false` serves the files as they\n\t * are.\n\t */\n\treadonly precompressed?: boolean;\n}\n\n/** The methods the catch-all takes. A `HEAD` reaches its `GET`. */\nconst METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;\n\n/** The handlers `reactRouter()` declared: the catch-all's and the client build's. */\nconst declared = new WeakSet<RouteDefinition['handler']>();\n\n/**\n * Whether `reactRouter()` declared this route: the catch-all, or one of the\n * client build's files. For `@alxia/openapi`'s `matchesSpec`, whose\n * `exclude` leaves them out, so the app's routes are checked against the\n * operations of its OpenAPI document and its pages are not.\n *\n * ```ts\n * matchesSpec(app, operations, { exclude: isReactRouterRoute });\n * ```\n */\nexport function isReactRouterRoute(route: RouteDefinition): boolean {\n\treturn declared.has(route.handler);\n}\n\n/**\n * A React Router framework app, server rendered on `app`: `GET`, `POST`,\n * `PUT`, `PATCH` and `DELETE` at `/*`, behind every middleware declared on `app`\n * before it. Each loader, action and middleware reads what those added\n * through `alxiaOf<App>(context)`. The app's own routes — an `/api`\n * — answer their paths, declared before it or after; the client build's\n * files are served when `client` is given.\n *\n * ```ts\n * const app = base.plugin((app) =>\n * reactRouter(app, { build: () => import('./build/server/index.js'), client: 'build/client' }),\n * );\n * ```\n */\nexport function reactRouter<Ctx extends object, Prefix extends string>(\n\tapp: Alxia<Ctx, Prefix>,\n\toptions: ReactRouterOptions<Ctx>,\n): Alxia<Ctx, Prefix> {\n\tconst mode = options.mode ?? 'production';\n\tconst handle = requestHandler(options.build, mode);\n\tconst getLoadContext = options.getLoadContext;\n\tconst handler = async (ctx: BaseContext & Ctx) => {\n\t\tconst context = new RouterContextProvider();\n\t\tcontext.set(alxiaContext, ctx);\n\t\tawait getLoadContext?.(ctx, context);\n\t\tconst response = await handle(routerRequest(ctx), context);\n\t\treturn ctx.reply(\n\t\t\tresponse.status as StatusCode,\n\t\t\tresponse.body ?? undefined,\n\t\t\t{ headers: response.headers },\n\t\t);\n\t};\n\n\tconst routes = asRoutes(app);\n\tdeclaring(app, () => {\n\t\tif (options.client !== undefined && mode === 'production') {\n\t\t\tserveClient(routes, pathOf(options.client), {\n\t\t\t\tprecompressed: options.precompressed ?? true,\n\t\t\t});\n\t\t}\n\t\tfor (const method of METHODS) routes[method]('/*', handler);\n\t});\n\treturn app;\n}\n\n/**\n * The request React Router reads: at the URL the client asked for, which\n * `originalUrl` reads behind the app's trusted proxies (`alxia({ proxy })`),\n * so `request.url` in a loader names the public scheme and host; and a\n * `HEAD` as its `GET`, since React Router answers a `HEAD` with no headers\n * at all, and the core drops the body.\n */\nfunction routerRequest(ctx: BaseContext): Request {\n\tconst href = originalUrl(ctx).href;\n\tconst head = ctx.request.method === 'HEAD';\n\tconst request =\n\t\thref === ctx.url.href ? ctx.request : new Request(href, ctx.request);\n\treturn head ? new Request(request, { method: 'GET' }) : request;\n}\n\n/**\n * Declares the client build's files on `app`, marked for\n * `isReactRouterRoute`: what `reactRouter()` does with `client`, apart, so\n * that `createServer()` serves them before the middlewares of `configure`.\n */\nexport function declareClient(\n\tapp: AnyAlxia,\n\tclient: string | URL,\n\tprecompressed = true,\n): void {\n\tdeclaring(app, () =>\n\t\tserveClient(asRoutes(app), pathOf(client), { precompressed }),\n\t);\n}\n\n/** Runs `declare`, and marks every route it added to `app` as ours. */\nfunction declaring(app: AnyAlxia, declare: () => void): void {\n\tconst before = new Set(app.routes);\n\tdeclare();\n\tfor (const route of app.routes) {\n\t\tif (!before.has(route)) declared.add(route.handler);\n\t}\n}\n\n/** The app's route methods, untyped: the catch-all's handler is not one a route type admits. */\nfunction asRoutes(app: AnyAlxia) {\n\treturn app as unknown as {\n\t\tstatic(path: string, source: string, options: object): unknown;\n\t\tfile(path: string, file: string, options: object): unknown;\n\t} & Record<\n\t\t(typeof METHODS)[number],\n\t\t(path: string, handler: unknown) => unknown\n\t>;\n}\n\n/** A handler that resolves a function build once in production, on every request in development. */\nfunction requestHandler(\n\tbuild: ReactRouterOptions<never>['build'],\n\tmode: 'development' | 'production',\n): RequestHandler {\n\tif (typeof build !== 'function' || mode === 'development') {\n\t\treturn createRequestHandler(build, mode);\n\t}\n\tlet resolved: Promise<RequestHandler> | undefined;\n\treturn async (request, context) => {\n\t\tresolved ??= Promise.resolve(build()).then(\n\t\t\t(server) => createRequestHandler(server, mode),\n\t\t\t(error: unknown) => {\n\t\t\t\tresolved = undefined;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\treturn (await resolved)(request, context);\n\t};\n}\n\nfunction pathOf(client: string | URL): string {\n\treturn client instanceof URL ? fileURLToPath(client) : client;\n}\n",
|
|
6
6
|
"/**\n * The client build's files, served before the catch-all: the hashed\n * `assets/` immutable, every other top-level file or folder — what React\n * Router copied from `public/` — with an hour's cache.\n */\nimport { readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** A hashed asset never changes under its name. */\nexport const IMMUTABLE = 'public, max-age=31536000, immutable';\n/** A public file keeps its name across builds: revalidated after an hour. */\nexport const AN_HOUR = 'public, max-age=3600';\n\n/** The copies the plugin writes, best first. */\nconst CODINGS = ['br', 'gzip'] as const;\n\ninterface Serving {\n\tstatic(path: string, source: string, options: object): unknown;\n\tfile(path: string, file: string, options: object): unknown;\n}\n\n/** Declares the client build's routes on `app`. */\nexport function serveClient(\n\tapp: Serving,\n\tclient: string,\n\toptions: { readonly precompressed?: boolean } = {},\n): void {\n\tconst precompressed =\n\t\toptions.precompressed === false ? {} : { precompressed: CODINGS };\n\tif (!isDirectory(client)) {\n\t\tthrow new TypeError(\n\t\t\t`reactRouter(): client is ${client}, which is not a directory. Pass the client build, build/client by default.`,\n\t\t);\n\t}\n\tfor (const name of readdirSync(client).sort()) {\n\t\tif (name.startsWith('.')) continue;\n\t\tconst path = join(client, name);\n\t\tconst cacheControl = name === 'assets' ? IMMUTABLE : AN_HOUR;\n\t\t// Declared as a request's URL carries the name: `my file.pdf` is\n\t\t// `/my%20file.pdf`, `café.png` is `/caf%C3%A9.png`.\n\t\tconst route = new URL(`/${name}`, 'http://localhost').pathname;\n\t\ttry {\n\t\t\tif (isDirectory(path))\n\t\t\t\tapp.static(route, path, { cacheControl, ...precompressed });\n\t\t\telse app.file(route, path, { cacheControl, ...precompressed });\n\t\t} catch (error) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`reactRouter(): ${path} cannot be served at a path of its own name; rename it. ${(error as Error).message}`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction isDirectory(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isDirectory();\n\t} catch {\n\t\treturn false;\n\t}\n}\n",
|
|
7
|
-
"/**\n * `createServer()`: the alxia server of a React Router app, as\n * `@alxia/react-router/vite` builds it. The plugin hands it React Router's\n * build, the mode and the client folder; the app's own `app/server.ts`\n * customises the rest, or is not written at all.\n */\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\talxia,\n\ttype ContextOf,\n\ttype Empty,\n\ttype ListenInfo,\n\ttype ListenOptions,\n\ttype MaybePromise,\n\ttype ProxyTrust,\n} from '@alxia/core';\nimport type { RouterContextProvider, ServerBuild } from 'react-router';\nimport { declareClient, reactRouter } from './react-router';\n\n/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */\nexport type FreshApp = Alxia<Empty, ''>;\n\n/**\n * What the Vite plugin hands the server, and a test passes to `create`:\n * React Router's server build, its mode, and the client build's folder.\n */\nexport interface ServerWiring {\n\t/**\n\t * React Router's server build, or a function that returns it: called on\n\t * every request in `development`, once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** `development` under `react-router dev`, `production` in a build, and by default. */\n\treadonly mode?: 'development' | 'production';\n\t/** The client build's folder, a path or a `file:` URL: served in `production`. */\n\treadonly client?: string | URL | undefined;\n}\n\nexport interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {\n\t/**\n\t * Runs first, on a new app: what is declared here runs before the\n\t * client's files too — a guard, a rate limit, a logger that should see\n\t * every request. Returns the app, so its types flow to `configure`.\n\t */\n\treadonly beforeAll?: (app: FreshApp) => Before;\n\t/**\n\t * The app the pages run behind: its middlewares, its plugins, its `/api`.\n\t * Declared after the client's files and before the catch-all. Returns\n\t * the app: what it builds is what the loaders read through `alxiaOf`.\n\t */\n\treadonly configure?: (app: Before) => App;\n\t/**\n\t * Sets the app's own keys on React Router's context provider, `ctx`\n\t * typed by `configure`'s app. `alxiaContext` is always set.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: ContextOf<App>,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The proxies in front of the app, `trustProxy({ trusted })` from\n\t * `@alxia/core`: `alxia({ proxy })`'s option, so `ctx.ip` is the\n\t * client's and React Router's `request.url` the URL it asked for.\n\t */\n\treadonly proxy?: ProxyTrust;\n\t/** Overrides the plugin's server build: rarely wanted. */\n\treadonly build?: ServerWiring['build'];\n\t/** Overrides the plugin's mode, `development` in dev and `production` in a build. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Overrides the plugin's client folder, `build/client` beside the built\n\t * server. `false` serves none of it: declare the files yourself, in\n\t * `beforeAll` or `configure`.\n\t */\n\treadonly client?: string | URL | false;\n\t/**\n\t * Serves the client's `.br` and `.gz` copies, which `alxia()` writes at\n\t * build time, to a browser that accepts them: `true` by default.\n\t * `false` serves the files as they are.\n\t */\n\treadonly precompressed?: boolean;\n\t/**\n\t * `listen`'s options for `bun build/server/index.js` — `shutdownTimeout`,\n\t * `signals` among them; a `port` or `hostname` here wins over `PORT` (3000)\n\t * and `HOST` (`0.0.0.0`) from the environment.\n\t */\n\treadonly listen?: ListenOptions;\n\t/**\n\t * Called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place.\n\t * Prints `alxia listening on <url>` by default, and in dev the route\n\t * table (`alxia({ dev })`). An `onListen` in `listen` wins over it.\n\t */\n\treadonly onListen?: (server: Bun.Server<unknown>) => void;\n}\n\n/**\n * The server `createServer()` describes, made into an app by the Vite\n * plugin, by a test, or by a server file of your own.\n */\nexport interface ReactRouterServer<App extends AnyAlxia> {\n\t/**\n\t * The app, serving React Router's build: `build/server/index.js`'s\n\t * default export. A test drives it with `app.request`.\n\t *\n\t * ```ts\n\t * const app = server.create({ build: await import('./build/server/index.js') });\n\t * ```\n\t */\n\tcreate(wiring: ServerWiring): App;\n\t/**\n\t * Listens with `app`, on `listen`, `PORT` and `HOST`, and shuts it down\n\t * gracefully on `SIGINT` or `SIGTERM`, as `@alxia/core`'s `listen` does:\n\t * the requests in flight finish within `shutdownTimeout`, its `onStop`\n\t * hooks run, and the process exits. What `bun build/server/index.js` runs.\n\t */\n\tstart(app: App): Bun.Server<unknown>;\n}\n\n/**\n * The alxia server of a React Router app, for `app/server.ts`. Every option\n * is optional: the Vite plugin wires React Router's build, the mode and the\n * client folder, and without the file uses `createServer()` as it is.\n *\n * ```ts\n * const server = createServer({\n * configure: (app) => app.use(logger()).get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n * });\n * export default server;\n * ```\n *\n * The request runs through `beforeAll`, the client's files, `configure`,\n * then the pages.\n */\nexport function createServer<\n\tBefore extends AnyAlxia = FreshApp,\n\tApp extends AnyAlxia = Before,\n>(options: ServerOptions<Before, App> = {}): ReactRouterServer<App> {\n\treturn {\n\t\tcreate(wiring) {\n\t\t\tconst mode = options.mode ?? wiring.mode ?? 'production';\n\t\t\tconst client =\n\t\t\t\toptions.client === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (options.client ?? wiring.client);\n\t\t\t// The app helps the developer in React Router's dev alone: a production\n\t\t\t// build is never in dev, whatever NODE_ENV says.\n\t\t\tconst fresh = alxia({\n\t\t\t\tdev: mode === 'development',\n\t\t\t\t...(options.proxy === undefined ? {} : { proxy: options.proxy }),\n\t\t\t});\n\t\t\t// Without beforeAll or configure, Before and App are their defaults,\n\t\t\t// the app passed through: a type argument given by hand is believed.\n\t\t\tconst before = (options.beforeAll?.(fresh) ?? fresh) as Before;\n\t\t\tif (client !== undefined && mode === 'production') {\n\t\t\t\tdeclareClient(before, client, options.precompressed ?? true);\n\t\t\t}\n\t\t\tconst app = (options.configure?.(before) ?? before) as App;\n\t\t\tconst getLoadContext = options.getLoadContext;\n\t\t\treactRouter(app, {\n\t\t\t\tbuild: options.build ?? wiring.build,\n\t\t\t\tmode,\n\t\t\t\t...(getLoadContext === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tgetLoadContext: (ctx, context) =>\n\t\t\t\t\t\t\t\tgetLoadContext(ctx as ContextOf<App>, context),\n\t\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn app;\n\t\t},\n\t\tstart(app) {\n\t\t\tconst own = options.listen?.onListen;\n\t\t\t// `listen` shuts the app down on SIGINT and SIGTERM — readiness 503,\n\t\t\t// the requests in flight drained, the onStop hooks, then the exit —\n\t\t\t// with its handlers in place before it tells onListen: a supervisor\n\t\t\t// may signal as soon as it reads that the server listens.\n\t\t\treturn app.listen({\n\t\t\t\tport: Number(process.env['PORT'] || 3000),\n\t\t\t\thostname: process.env['HOST'] || '0.0.0.0',\n\t\t\t\t...options.listen,\n\t\t\t\tonListen: (info) => {\n\t\t\t\t\tif (own !== undefined) own(info);\n\t\t\t\t\telse if (options.onListen !== undefined) {\n\t\t\t\t\t\toptions.onListen(info.server);\n\t\t\t\t\t} else announce(info);\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** In dev, the route table `listen` prints; else one line. */\nfunction announce({ dev, table, url }: ListenInfo): void {\n\tconsole.log(dev ? table : `alxia listening on ${url}`);\n}\n",
|
|
7
|
+
"/**\n * `createServer()`: the alxia server of a React Router app, as\n * `@alxia/react-router/vite` builds it. The plugin hands it React Router's\n * build, the mode and the client folder; the app's own `app/server.ts`\n * customises the rest, or is not written at all.\n */\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\talxia,\n\ttype ContextOf,\n\ttype Empty,\n\ttype ListenInfo,\n\ttype ListenOptions,\n\ttype MaybePromise,\n\ttype ProxyTrust,\n} from '@alxia/core';\nimport type { RouterContextProvider, ServerBuild } from 'react-router';\nimport { declareHealth, type ServerHealth } from './health';\nimport { declareClient, reactRouter } from './react-router';\n\n/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */\nexport type FreshApp = Alxia<Empty, ''>;\n\n/**\n * What the Vite plugin hands the server, and a test passes to `create`:\n * React Router's server build, its mode, and the client build's folder.\n */\nexport interface ServerWiring {\n\t/**\n\t * React Router's server build, or a function that returns it: called on\n\t * every request in `development`, once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** `development` under `react-router dev`, `production` in a build, and by default. */\n\treadonly mode?: 'development' | 'production';\n\t/** The client build's folder, a path or a `file:` URL: served in `production`. */\n\treadonly client?: string | URL | undefined;\n}\n\nexport interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {\n\t/**\n\t * A health probe, declared before everything else: before `beforeAll`,\n\t * the client's files and every middleware, so it needs no session, sets\n\t * no cookie and renders nothing: `@alxia/core`'s `health()` in text,\n\t * liveness alone (no `/ready`). `GET` answers `200` `ok` as `text/plain`,\n\t * or `503` `unavailable` when `check` fails, always with\n\t * `cache-control: no-store`. A path, or `{ path, check }`.\n\t */\n\treadonly health?: `/${string}` | ServerHealth;\n\t/**\n\t * Runs first, on a new app: what is declared here runs before the\n\t * client's files too — a guard, a rate limit, a logger that should see\n\t * every request. Returns the app, so its types flow to `configure`.\n\t */\n\treadonly beforeAll?: (app: FreshApp) => Before;\n\t/**\n\t * The app the pages run behind: its middlewares, its plugins, its `/api`.\n\t * Declared after the client's files and before the catch-all. Returns\n\t * the app: what it builds is what the loaders read through `alxiaOf`.\n\t */\n\treadonly configure?: (app: Before) => App;\n\t/**\n\t * Sets the app's own keys on React Router's context provider, `ctx`\n\t * typed by `configure`'s app. `alxiaContext` is always set.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: ContextOf<App>,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The proxies in front of the app, `trustProxy({ trusted })` from\n\t * `@alxia/core`: `alxia({ proxy })`'s option, so `ctx.ip` is the\n\t * client's and React Router's `request.url` the URL it asked for.\n\t */\n\treadonly proxy?: ProxyTrust;\n\t/** Overrides the plugin's server build: rarely wanted. */\n\treadonly build?: ServerWiring['build'];\n\t/** Overrides the plugin's mode, `development` in dev and `production` in a build. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Overrides the plugin's client folder, `build/client` beside the built\n\t * server. `false` serves none of it: declare the files yourself, in\n\t * `beforeAll` or `configure`.\n\t */\n\treadonly client?: string | URL | false;\n\t/**\n\t * Serves the client's `.br` and `.gz` copies, which `alxia()` writes at\n\t * build time, to a browser that accepts them: `true` by default.\n\t * `false` serves the files as they are.\n\t */\n\treadonly precompressed?: boolean;\n\t/**\n\t * `listen`'s options for `bun build/server/index.js` — `shutdownTimeout`,\n\t * `signals` among them; a `port` or `hostname` here wins over `PORT` (3000)\n\t * and `HOST` (`0.0.0.0`) from the environment.\n\t */\n\treadonly listen?: ListenOptions;\n\t/**\n\t * Called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place.\n\t * Prints `alxia listening on <url>` by default, and in dev the route\n\t * table (`alxia({ dev })`). An `onListen` in `listen` wins over it.\n\t */\n\treadonly onListen?: (server: Bun.Server<unknown>) => void;\n}\n\n/**\n * The server `createServer()` describes, made into an app by the Vite\n * plugin, by a test, or by a server file of your own.\n */\nexport interface ReactRouterServer<App extends AnyAlxia> {\n\t/**\n\t * The app, serving React Router's build: `build/server/index.js`'s\n\t * default export. A test drives it with `app.request`.\n\t *\n\t * ```ts\n\t * const app = server.create({ build: await import('./build/server/index.js') });\n\t * ```\n\t */\n\tcreate(wiring: ServerWiring): App;\n\t/**\n\t * Listens with `app`, on `listen`, `PORT` and `HOST`, and shuts it down\n\t * gracefully on `SIGINT` or `SIGTERM`, as `@alxia/core`'s `listen` does:\n\t * the requests in flight finish within `shutdownTimeout`, its `onStop`\n\t * hooks run, and the process exits. What `bun build/server/index.js` runs.\n\t */\n\tstart(app: App): Bun.Server<unknown>;\n}\n\n/**\n * The alxia server of a React Router app, for `app/server.ts`. Every option\n * is optional: the Vite plugin wires React Router's build, the mode and the\n * client folder, and without the file uses `createServer()` as it is.\n *\n * ```ts\n * const server = createServer({\n * configure: (app) => app.use(logger()).get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n * });\n * export default server;\n * ```\n *\n * The request runs through `beforeAll`, the client's files, `configure`,\n * then the pages.\n */\nexport function createServer<\n\tBefore extends AnyAlxia = FreshApp,\n\tApp extends AnyAlxia = Before,\n>(options: ServerOptions<Before, App> = {}): ReactRouterServer<App> {\n\treturn {\n\t\tcreate(wiring) {\n\t\t\tconst mode = options.mode ?? wiring.mode ?? 'production';\n\t\t\tconst client =\n\t\t\t\toptions.client === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (options.client ?? wiring.client);\n\t\t\t// The app helps the developer in React Router's dev alone: a production\n\t\t\t// build is never in dev, whatever NODE_ENV says.\n\t\t\tconst fresh = alxia({\n\t\t\t\tdev: mode === 'development',\n\t\t\t\t...(options.proxy === undefined ? {} : { proxy: options.proxy }),\n\t\t\t});\n\t\t\tif (options.health !== undefined) declareHealth(fresh, options.health);\n\t\t\t// Without beforeAll or configure, Before and App are their defaults,\n\t\t\t// the app passed through: a type argument given by hand is believed.\n\t\t\tconst before = (options.beforeAll?.(fresh) ?? fresh) as Before;\n\t\t\tif (client !== undefined && mode === 'production') {\n\t\t\t\tdeclareClient(before, client, options.precompressed ?? true);\n\t\t\t}\n\t\t\tconst app = (options.configure?.(before) ?? before) as App;\n\t\t\tconst getLoadContext = options.getLoadContext;\n\t\t\treactRouter(app, {\n\t\t\t\tbuild: options.build ?? wiring.build,\n\t\t\t\tmode,\n\t\t\t\t...(getLoadContext === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tgetLoadContext: (ctx, context) =>\n\t\t\t\t\t\t\t\tgetLoadContext(ctx as ContextOf<App>, context),\n\t\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn app;\n\t\t},\n\t\tstart(app) {\n\t\t\tconst own = options.listen?.onListen;\n\t\t\t// `listen` shuts the app down on SIGINT and SIGTERM — readiness 503,\n\t\t\t// the requests in flight drained, the onStop hooks, then the exit —\n\t\t\t// with its handlers in place before it tells onListen: a supervisor\n\t\t\t// may signal as soon as it reads that the server listens.\n\t\t\treturn app.listen({\n\t\t\t\tport: Number(process.env['PORT'] || 3000),\n\t\t\t\thostname: process.env['HOST'] || '0.0.0.0',\n\t\t\t\t...options.listen,\n\t\t\t\tonListen: (info) => {\n\t\t\t\t\tif (own !== undefined) own(info);\n\t\t\t\t\telse if (options.onListen !== undefined) {\n\t\t\t\t\t\toptions.onListen(info.server);\n\t\t\t\t\t} else announce(info);\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** In dev, the route table `listen` prints; else one line. */\nfunction announce({ dev, table, url }: ListenInfo): void {\n\tconsole.log(dev ? table : `alxia listening on ${url}`);\n}\n",
|
|
8
|
+
"/**\n * `createServer({ health })`: the liveness probe, declared first. It is\n * `@alxia/core`'s `health()` mounted alone, in text.\n */\nimport { type AnyAlxia, health, type MaybePromise } from '@alxia/core';\n\n/** `createServer({ health })` in its long form. */\nexport interface ServerHealth {\n\t/** Where the probe answers, `GET`: `/health` by default. */\n\treadonly path?: `/${string}`;\n\t/**\n\t * Called on each probe: the probe answers 503 `unavailable` when it\n\t * returns `false`, throws, rejects or outlasts `timeout`; `undefined` or\n\t * no return value counts as healthy. The error is not logged.\n\t */\n\treadonly check?: () => MaybePromise<boolean | undefined | void>;\n\t/** How long `check` may take, in milliseconds: 1 000 by default. */\n\treadonly timeout?: number;\n}\n\n/** Declares the probe on `app`, before anything else. */\nexport function declareHealth(\n\tapp: AnyAlxia,\n\toption: `/${string}` | ServerHealth,\n): void {\n\tconst { path, check, timeout } =\n\t\ttypeof option === 'string' ? ({ path: option } as ServerHealth) : option;\n\tapp.plugin(\n\t\thealth({\n\t\t\tformat: 'text',\n\t\t\tready: false,\n\t\t\t...(path === undefined ? {} : { path }),\n\t\t\t...(check === undefined ? {} : { live: check }),\n\t\t\t...(timeout === undefined ? {} : { timeout }),\n\t\t}),\n\t);\n}\n",
|
|
8
9
|
"/**\n * `withAlxia(fn)`: a loader, an action or a middleware handed the alxia\n * context as `alxia`, typed even under the generated `Route.*Args`.\n */\nimport type { AnyAlxia } from '@alxia/core';\nimport {\n\ttype AlxiaContextOf,\n\talxiaOf,\n\ttype ProviderLike,\n\ttype RegisteredApp,\n} from './context';\nimport type { ReactRouterServer } from './server';\n\n/**\n * What `withAlxia` adds to React Router's arguments: `alxia`, what\n * `alxiaOf<App>(context)` returns. Typed by `Register` by default; name a\n * server, `AlxiaArgs<typeof server>`, as `alxiaOf<typeof server>` does.\n */\nexport interface AlxiaArgs<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n> {\n\treadonly alxia: AlxiaContextOf<App>;\n}\n\n/**\n * A loader, an action or a middleware given `alxia`, the alxia context, beside\n * React Router's arguments. Annotate the parameter as the generated arguments\n * and `AlxiaArgs`:\n *\n * ```ts\n * export const loader = withAlxia(({ alxia, params }: Route.LoaderArgs & AlxiaArgs) => {\n * return { name: alxia.user?.name ?? null, id: params.id };\n * });\n * ```\n *\n * It returns a function of React Router's arguments alone (`Args` without\n * `alxia`), with `fn`'s return type, so `useLoaderData<typeof loader>` and `Route.ComponentProps` read what\n * `fn` returns. A middleware's `next` is passed on. Throws `alxiaOf`'s error\n * on a request that did not come through `reactRouter()`.\n */\nexport function withAlxia<\n\t// `unknown`, not `AlxiaArgs<AnyAlxia>`, whose `alxia` is `any`: an `fn`\n\t// left unannotated reads nothing of it.\n\tArgs extends { readonly alxia: unknown; readonly context: ProviderLike },\n\tRest extends unknown[],\n\tResult,\n>(\n\tfn: (args: Args, ...rest: Rest) => Result,\n): (args: Omit<Args, 'alxia'>, ...rest: Rest) => Result {\n\treturn (args, ...rest) =>\n\t\tfn({ ...args, alxia: alxiaOf(args.context) } as unknown as Args, ...rest);\n}\n"
|
|
9
10
|
],
|
|
10
|
-
"mappings": "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
|
|
11
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "BACF49A4D19535D964756E2164756E21",
|
|
12
13
|
"names": []
|
|
13
14
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type Alxia, type AnyAlxia, type ContextOf, type Empty, type ListenOptions, type MaybePromise, type ProxyTrust } from '@alxia/core';
|
|
8
8
|
import type { RouterContextProvider, ServerBuild } from 'react-router';
|
|
9
|
+
import { type ServerHealth } from './health';
|
|
9
10
|
/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */
|
|
10
11
|
export type FreshApp = Alxia<Empty, ''>;
|
|
11
12
|
/**
|
|
@@ -24,6 +25,15 @@ export interface ServerWiring {
|
|
|
24
25
|
readonly client?: string | URL | undefined;
|
|
25
26
|
}
|
|
26
27
|
export interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {
|
|
28
|
+
/**
|
|
29
|
+
* A health probe, declared before everything else: before `beforeAll`,
|
|
30
|
+
* the client's files and every middleware, so it needs no session, sets
|
|
31
|
+
* no cookie and renders nothing: `@alxia/core`'s `health()` in text,
|
|
32
|
+
* liveness alone (no `/ready`). `GET` answers `200` `ok` as `text/plain`,
|
|
33
|
+
* or `503` `unavailable` when `check` fails, always with
|
|
34
|
+
* `cache-control: no-store`. A path, or `{ path, check }`.
|
|
35
|
+
*/
|
|
36
|
+
readonly health?: `/${string}` | ServerHealth;
|
|
27
37
|
/**
|
|
28
38
|
* Runs first, on a new app: what is declared here runs before the
|
|
29
39
|
* client's files too — a guard, a rate limit, a logger that should see
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,SAAS,EACd,KAAK,KAAK,EAEV,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,SAAS,EACd,KAAK,KAAK,EAEV,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAG5D,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAExC;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,uFAAuF;IACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa,CAAC,MAAM,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ;IAC3E;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,MAAM,EAAE,GAAG,YAAY,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,GAAG,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACzB,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EACnB,OAAO,EAAE,qBAAqB,KAC1B,YAAY,CAAC,IAAI,CAAC,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,GAAG,SAAS,QAAQ;IACtD;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC;IAClC;;;;;OAKG;IACH,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC3B,MAAM,SAAS,QAAQ,GAAG,QAAQ,EAClC,GAAG,SAAS,QAAQ,GAAG,MAAM,EAC5B,OAAO,GAAE,aAAa,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAsDlE"}
|
|
@@ -110,6 +110,7 @@ Every option is optional:
|
|
|
110
110
|
| option | what it does |
|
|
111
111
|
| --- | --- |
|
|
112
112
|
| `configure(app)` | the app the pages run behind: its middlewares and `/api`. It returns the app, and what it builds is what the loaders read |
|
|
113
|
+
| `health` | `'/health'` or `{ path?, check?, timeout? }`: a liveness probe declared before everything else; see [A health probe](#a-health-probe) |
|
|
113
114
|
| `beforeAll(app)` | runs first, on a new app. What it declares applies to the client's files too: a rate limit, a guard on everything, a logger that should see every asset. It returns the app, which `configure` then receives |
|
|
114
115
|
| `getLoadContext(ctx, context)` | sets the app's own keys on React Router's provider, `ctx` typed by `configure`'s app |
|
|
115
116
|
| `build`, `mode`, `client` | override what the plugin wires; see [Escape hatches](#escape-hatches) |
|
|
@@ -153,6 +154,30 @@ export default createServer({
|
|
|
153
154
|
is a compile error. A middleware that needs something an earlier one
|
|
154
155
|
derived is typed by the app it is given, as anywhere in alxia.
|
|
155
156
|
|
|
157
|
+
## A health probe
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
createServer({ health: { path: '/health', check: () => db.ping() } });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`GET /health` answers `200` `ok` as `text/plain` with `cache-control:
|
|
164
|
+
no-store`, or `503` `unavailable` when `check` returns `false`, throws,
|
|
165
|
+
rejects or outlasts `timeout` (1 000 ms by default); `undefined` or no
|
|
166
|
+
return value counts as healthy, and the error is not logged. It is declared
|
|
167
|
+
before `beforeAll`, the client's files and every middleware, so it renders
|
|
168
|
+
nothing, reads no session and sets no cookie. It is liveness only: it is
|
|
169
|
+
`@alxia/core`'s `health()` with `format: 'text'` and `ready: false`, and, as
|
|
170
|
+
that one, left out by `matchesSpec`. For `/ready` and its checks mount
|
|
171
|
+
`health()` yourself in `beforeAll`, on another `path`: it declares `/health`
|
|
172
|
+
too, and the app refuses a route declared twice.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
createServer({
|
|
176
|
+
health: '/health',
|
|
177
|
+
beforeAll: (app) => app.plugin(health({ path: '/live' })), // `health` from @alxia/core
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
156
181
|
## Behind a proxy
|
|
157
182
|
|
|
158
183
|
A proxy or a load balancer that terminates TLS asks the server over plain
|
package/docs/guide/deploying.md
CHANGED
|
@@ -95,5 +95,8 @@ startup, where Bun would otherwise fetch it from npm.
|
|
|
95
95
|
|
|
96
96
|
`PORT` and `HOST` set where the server listens. The platform's `SIGTERM`
|
|
97
97
|
stops it once the requests in flight are answered, within
|
|
98
|
-
`shutdownTimeout`, and runs the app's `onStop` hooks.
|
|
99
|
-
|
|
98
|
+
`shutdownTimeout`, and runs the app's `onStop` hooks. The short route to a liveness probe is
|
|
99
|
+
`createServer({ health: '/health' })`, declared before every middleware;
|
|
100
|
+
give the platform a readiness probe with `@alxia/core`'s `health()`, mounted
|
|
101
|
+
in `beforeAll` on another `path` (`health({ path: '/live' })`: it declares
|
|
102
|
+
`/health` too, which the option already took).
|
package/docs/roadmap.md
CHANGED
|
@@ -86,6 +86,13 @@ Nothing planned.
|
|
|
86
86
|
|
|
87
87
|
## Shipped
|
|
88
88
|
|
|
89
|
+
### Unreleased
|
|
90
|
+
|
|
91
|
+
- **A health probe in `createServer`.** `health: '/health'` or `{ path,
|
|
92
|
+
check, timeout }` declares `@alxia/core`'s liveness probe, in text, before
|
|
93
|
+
every middleware. See [A health
|
|
94
|
+
probe](guide/customising-the-server.md#a-health-probe).
|
|
95
|
+
|
|
89
96
|
### 0.14.0
|
|
90
97
|
|
|
91
98
|
- **`@alxia/react-router/session` and `/session/model`: a BFF session for a
|
package/docs/troubleshooting.md
CHANGED
|
@@ -93,6 +93,8 @@ a loader, a message React Router or the browser prints, or an error from
|
|
|
93
93
|
- [`TypeError: spendToken(): a token page's path must be a path of this site with no query or fragment, starting with a single "/"; got ….`](#typeerror-spendtoken-a-token-pages-path-must-be-a-path-of-this-site-with-no-query-or-fragment-starting-with-a-single--got-)
|
|
94
94
|
- [`TypeError: authLinks(): resetDone must be a path of this site, starting with a single "/"; got ….`](#typeerror-authlinks-resetdone-must-be-a-path-of-this-site-starting-with-a-single--got-)
|
|
95
95
|
- [`Type '…' is not assignable to type 'undefined'` passing a context to a step](#type--is-not-assignable-to-type-undefined-passing-a-context-to-a-step)
|
|
96
|
+
- [`503 unavailable` from the `health` probe](#503-unavailable-from-the-health-probe)
|
|
97
|
+
- [`GET /health is declared twice: keep one; …`](#get-health-is-declared-twice-keep-one-)
|
|
96
98
|
|
|
97
99
|
**Types**
|
|
98
100
|
|
|
@@ -142,6 +144,47 @@ a loader, a message React Router or the browser prints, or an error from
|
|
|
142
144
|
|
|
143
145
|
## Thrown or printed
|
|
144
146
|
|
|
147
|
+
### `503 unavailable` from the `health` probe
|
|
148
|
+
|
|
149
|
+
`createServer({ health: { check } })` answers `503` `unavailable` when
|
|
150
|
+
`check` returned `false`, threw, rejected, or did not settle within
|
|
151
|
+
`timeout` (1 000 ms by default). The error is not logged: call the check
|
|
152
|
+
yourself to see it. A `check` that returns nothing counts as healthy.
|
|
153
|
+
|
|
154
|
+
Two traps:
|
|
155
|
+
|
|
156
|
+
- A `/health` declared in `configure` or `beforeAll`, or core's `health()`
|
|
157
|
+
mounted there with its default path, is not shadowed: the app throws at
|
|
158
|
+
startup, see the next entry.
|
|
159
|
+
- A check that never settles holds the probe until `timeout`, then answers
|
|
160
|
+
503. Lower `timeout` if the platform's probe gives up sooner.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
createServer({ health: { check: () => db.ping(), timeout: 500 } });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `GET /health is declared twice: keep one; …`
|
|
167
|
+
|
|
168
|
+
**When:** `createServer({ health })` and another route or plugin declare the
|
|
169
|
+
same path: a `/health` route in `configure` or `beforeAll`, or `health()`
|
|
170
|
+
mounted with its default `path`. The server throws when it is created.
|
|
171
|
+
|
|
172
|
+
**Why:** the probe is declared first, and a method and path may be declared
|
|
173
|
+
once.
|
|
174
|
+
|
|
175
|
+
**Fix:** keep one. Drop the hand-written route, or give the other one
|
|
176
|
+
another path:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
createServer({
|
|
180
|
+
health: '/health',
|
|
181
|
+
beforeAll: (app) => app.plugin(health({ path: '/live' })),
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
See also core's [declared
|
|
186
|
+
twice](https://github.com/softistx/alxia/blob/develop/packages/core/docs/troubleshooting.md#get--is-declared-twice).
|
|
187
|
+
|
|
145
188
|
### `Not a step-up intent` (a 400 from `stepUp.route`)
|
|
146
189
|
|
|
147
190
|
**When:** a page posts to the route that serves `stepUpSteps().route` with an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/react-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served; the code with no UI an installable app shares (`/pwa`), its hooks (`/pwa/react`), `noCache` for the worker's script (`/pwa/server`), the service worker on Workbox (`/pwa/worker`) and its build (`/pwa/build`), an API's refusals worded for the page (`/errors`), the BFF session: cookies, sign-in, step-up and mail links (`/session`), its page hooks (`/session/react`) and what it shares with the page (`/session/model`), a GraphQL subscription relayed to the browser over SSE (`/subscription`), the page's CSP nonce and inline boot scripts (`/document`) and a streaming `entry.server` (`/entry`)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -141,9 +141,9 @@
|
|
|
141
141
|
]
|
|
142
142
|
},
|
|
143
143
|
"devDependencies": {
|
|
144
|
-
"@alxia/compress": "^0.3.
|
|
145
|
-
"@alxia/core": "^0.
|
|
146
|
-
"@alxia/openapi": "^0.6.
|
|
144
|
+
"@alxia/compress": "^0.3.13",
|
|
145
|
+
"@alxia/core": "^0.18.0",
|
|
146
|
+
"@alxia/openapi": "^0.6.13",
|
|
147
147
|
"@nxgt/graphql-client": "^0.6.0",
|
|
148
148
|
"@nxgt/janus": "^0.29.0",
|
|
149
149
|
"@react-router/dev": "^8.5.0",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"zod": "^4.2.0"
|
|
168
168
|
},
|
|
169
169
|
"peerDependencies": {
|
|
170
|
-
"@alxia/core": "^0.
|
|
170
|
+
"@alxia/core": "^0.18.0",
|
|
171
171
|
"isbot": "^5.0.0",
|
|
172
172
|
"react": "^19.0.0",
|
|
173
173
|
"react-dom": "^19.0.0",
|