@alxia/react-router 0.11.0 → 0.12.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 +194 -19
- package/dist/chunks/index-5rt6hnhn.js +42 -0
- package/dist/chunks/index-5rt6hnhn.js.map +11 -0
- package/dist/chunks/{index-mppz0806.js → index-6jf7zpxz.js} +5 -5
- package/dist/chunks/{index-mppz0806.js.map → index-6jf7zpxz.js.map} +1 -1
- package/dist/chunks/index-y6vpnjr3.js +123 -0
- package/dist/chunks/index-y6vpnjr3.js.map +14 -0
- package/dist/context.d.ts +0 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/document/index.d.ts +11 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +19 -0
- package/dist/document/index.js.map +9 -0
- package/dist/document/inline-script.d.ts +27 -0
- package/dist/document/inline-script.d.ts.map +1 -0
- package/dist/document/language-store.d.ts +8 -0
- package/dist/document/language-store.d.ts.map +1 -0
- package/dist/document/nonce.d.ts +34 -0
- package/dist/document/nonce.d.ts.map +1 -0
- package/dist/document/page-language.d.ts +39 -0
- package/dist/document/page-language.d.ts.map +1 -0
- package/dist/document/text-direction.d.ts +10 -0
- package/dist/document/text-direction.d.ts.map +1 -0
- package/dist/entry/abort.d.ts +21 -0
- package/dist/entry/abort.d.ts.map +1 -0
- package/dist/entry/create-entry.d.ts +32 -0
- package/dist/entry/create-entry.d.ts.map +1 -0
- package/dist/entry/index.d.ts +8 -0
- package/dist/entry/index.d.ts.map +1 -0
- package/dist/entry/index.js +128 -0
- package/dist/entry/index.js.map +12 -0
- package/dist/entry/whole-page.d.ts +10 -0
- package/dist/entry/whole-page.d.ts.map +1 -0
- package/dist/index.js +13 -34
- package/dist/index.js.map +3 -5
- package/dist/pwa/build/index.js +1 -1
- package/dist/pwa/index.js +6 -6
- package/dist/pwa/index.js.map +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +29 -2
- package/dist/vite/index.js.map +5 -4
- package/dist/vite/inline.d.ts +18 -0
- package/dist/vite/inline.d.ts.map +1 -0
- package/docs/README.md +1 -0
- package/docs/guide/csp-nonce.md +35 -3
- package/docs/guide/document.md +310 -0
- package/docs/roadmap.md +17 -1
- package/docs/troubleshooting.md +231 -1
- package/package.json +22 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The picked language and who listens to it: internal to `createPageLanguage`. */
|
|
2
|
+
export interface LanguageStore<L extends string> {
|
|
3
|
+
subscribe(listener: () => void): () => void;
|
|
4
|
+
getSnapshot(): L | undefined;
|
|
5
|
+
choose(language: L): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createLanguageStore<L extends string>(): LanguageStore<L>;
|
|
8
|
+
//# sourceMappingURL=language-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-store.d.ts","sourceRoot":"","sources":["../../src/document/language-store.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,MAAM;IAC9C,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,WAAW,IAAI,CAAC,GAAG,SAAS,CAAC;IAC7B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,CAgBxE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface DocumentNonceProps {
|
|
3
|
+
/** This response's CSP nonce: `nonceOf(loadContext)` in the entry. */
|
|
4
|
+
nonce: string | undefined;
|
|
5
|
+
children?: ReactNode | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Gives the server render this response's CSP nonce, for `useNonce()` and
|
|
9
|
+
* the components built on it. The entry wraps the router in it, so every
|
|
10
|
+
* boundary reads the nonce, an error page after an action included (it
|
|
11
|
+
* runs no loader). Never put the nonce in loader data: the browser hides
|
|
12
|
+
* it from the page, and loader data would print it in the hydration script
|
|
13
|
+
* and every `.data` answer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function DocumentNonce({ nonce, children, }: DocumentNonceProps): ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* The page's CSP nonce, for what the root renders: an inline script, and
|
|
18
|
+
* React Router's `<Links>`, `<Scripts>` and `<ScrollRestoration>`. On the
|
|
19
|
+
* server, the one `DocumentNonce` provides. In the browser, the one the
|
|
20
|
+
* document carries: React Router's client knows no nonce, and React would
|
|
21
|
+
* find `nonce` on the server's elements and none in its own (a hydration
|
|
22
|
+
* mismatch). It is read from the DOM property of the first script or
|
|
23
|
+
* `csp-nonce` meta in the `<head>` (the browser hides the attribute, not
|
|
24
|
+
* the property), else of the first `script[nonce]` anywhere, once for the
|
|
25
|
+
* document. That first read is cached per module copy, so a test with a
|
|
26
|
+
* DOM keeps it across tests: reset modules, or render the server path with
|
|
27
|
+
* `DocumentNonce`.
|
|
28
|
+
*
|
|
29
|
+
* "In the browser" means a global `document`. A server that installs a DOM
|
|
30
|
+
* shim on `globalThis` takes the browser path and finds no nonce: it must not
|
|
31
|
+
* render this hook that way.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useNonce(): string | undefined;
|
|
34
|
+
//# sourceMappingURL=nonce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../../src/document/nonce.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,SAAS,EAEd,MAAM,OAAO,CAAC;AAiBf,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC7B,KAAK,EACL,QAAQ,GACR,EAAE,kBAAkB,GAAG,SAAS,CAEhC;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,SAAS,CAS7C"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PageLanguageOptions<L extends string> {
|
|
2
|
+
/** The languages the app serves. */
|
|
3
|
+
readonly languages: readonly L[];
|
|
4
|
+
/** The language of a page whose root data gives none; one of `languages`. */
|
|
5
|
+
readonly fallback: NoInfer<L>;
|
|
6
|
+
/**
|
|
7
|
+
* Whether this is a browser. Default: `typeof document !== 'undefined'`,
|
|
8
|
+
* a global `document`: a server with a DOM shim on `globalThis` must pass
|
|
9
|
+
* `() => false`, or the instance remembers a language across requests.
|
|
10
|
+
*/
|
|
11
|
+
readonly browser?: (() => boolean) | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface PageLanguage<L extends string> {
|
|
14
|
+
/**
|
|
15
|
+
* The page's language from the root loader's `language`. In the browser,
|
|
16
|
+
* the one picked with `chooseLanguage` wins, and the last one seen stands
|
|
17
|
+
* in when the root's data is gone (a failed navigation takes the root's
|
|
18
|
+
* with it): the error page keeps the page's language, not the fallback's.
|
|
19
|
+
* On the server it remembers nothing, since one module serves every request.
|
|
20
|
+
*/
|
|
21
|
+
pageLanguage(language: string | undefined): L;
|
|
22
|
+
/** The page's language in a route's `meta`, from `matches[0].loaderData.language`. */
|
|
23
|
+
metaLanguage(matches: ReadonlyArray<unknown>): L;
|
|
24
|
+
/**
|
|
25
|
+
* Records the language picked in a language menu: from then on the page's
|
|
26
|
+
* language in the browser, until the next document. Re-renders every
|
|
27
|
+
* `useChosenLanguage()`.
|
|
28
|
+
*/
|
|
29
|
+
chooseLanguage(language: L): void;
|
|
30
|
+
/** The language picked in this document, if any; re-renders on a new pick. */
|
|
31
|
+
useChosenLanguage(): L | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The page's language for a root route: `<html lang>`, a route's `meta`, and
|
|
35
|
+
* the language menu. An instance, not module state: keep one per app, in a
|
|
36
|
+
* module both the root and the routes import.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createPageLanguage<const L extends string>(options: PageLanguageOptions<L>): PageLanguage<L>;
|
|
39
|
+
//# sourceMappingURL=page-language.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-language.d.ts","sourceRoot":"","sources":["../../src/document/page-language.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM;IACpD,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,GAAG,SAAS,CAAC;CAC/C;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM;IAC7C;;;;;;OAMG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC;IAC9C,sFAAsF;IACtF,YAAY,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjD;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;IAClC,8EAA8E;IAC9E,iBAAiB,IAAI,CAAC,GAAG,SAAS,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EACxD,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC7B,YAAY,CAAC,CAAC,CAAC,CA+BjB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `dir` of a language tag: `'rtl'` for a right-to-left base language,
|
|
3
|
+
* `'ltr'` for the rest. The base tag is read whatever its case, so `ar-EG`
|
|
4
|
+
* and `AR` are `'rtl'` and `en-US` is `'ltr'`.
|
|
5
|
+
*
|
|
6
|
+
* Known gap: a script subtag is not read, so `az-Arab` (Azerbaijani in the
|
|
7
|
+
* Arabic script) is `'ltr'`, as `az` is.
|
|
8
|
+
*/
|
|
9
|
+
export declare function textDirection(language: string): 'ltr' | 'rtl';
|
|
10
|
+
//# sourceMappingURL=text-direction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-direction.d.ts","sourceRoot":"","sources":["../../src/document/text-direction.ts"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,CAG7D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The render's abort timer: one per response, cleared when the response is over. */
|
|
2
|
+
export interface Deadline {
|
|
3
|
+
readonly signal: AbortSignal;
|
|
4
|
+
/** Stops the timer. Calling it again does nothing. */
|
|
5
|
+
readonly clear: () => void;
|
|
6
|
+
}
|
|
7
|
+
/** A signal aborted after `ms`, with the means to stop the timer. */
|
|
8
|
+
export declare function deadline(ms: number): Deadline;
|
|
9
|
+
/**
|
|
10
|
+
* The same bytes in a new stream that calls `done` when the body ends,
|
|
11
|
+
* fails or is cancelled (the client went away), so no timer outlives the
|
|
12
|
+
* response. React's `allReady` is not carried over: await it first.
|
|
13
|
+
*/
|
|
14
|
+
export declare function untilDone(body: ReadableStream<Uint8Array>, done: () => void): ReadableStream<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* Gives up on a render that will not be served: stops its timer and cancels
|
|
17
|
+
* the body, which aborts React's work. A cancel that itself fails is ignored,
|
|
18
|
+
* so the error that got us here is the one the caller rethrows.
|
|
19
|
+
*/
|
|
20
|
+
export declare function abandon(limit: Pick<Deadline, 'clear'>, body: Pick<ReadableStream, 'cancel'> | undefined): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=abort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../src/entry/abort.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,CAI7C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,IAAI,EAAE,MAAM,IAAI,GACd,cAAc,CAAC,UAAU,CAAC,CAsB5B;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC5B,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAC9B,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS,GAC9C,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type EntryContext, type RouterContextProvider } from 'react-router';
|
|
2
|
+
export interface EntryOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Milliseconds React Router waits for a pending promise; the render itself
|
|
5
|
+
* is aborted 1 000 ms later. Export it as `streamTimeout`. Default 5 000.
|
|
6
|
+
*/
|
|
7
|
+
readonly streamTimeout?: number | undefined;
|
|
8
|
+
/** This response's CSP nonce. Default `nonceOf(loadContext)`. */
|
|
9
|
+
readonly nonce?: ((loadContext: RouterContextProvider, request: Request) => string | undefined) | undefined;
|
|
10
|
+
/** Whether a user agent is a bot, which gets the whole page. Default `isbot`, which is then needed. */
|
|
11
|
+
readonly isBot?: ((userAgent: string) => boolean) | undefined;
|
|
12
|
+
/** An error after the shell is out. Default `console.error`. */
|
|
13
|
+
readonly onError?: ((error: unknown) => void) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** React Router's `handleRequest` export of `entry.server`. */
|
|
16
|
+
export type HandleRequest = (request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, loadContext: RouterContextProvider) => Promise<Response>;
|
|
17
|
+
/** What `createEntry` returns: the two exports of `entry.server`. */
|
|
18
|
+
export interface Entry {
|
|
19
|
+
readonly handleRequest: HandleRequest;
|
|
20
|
+
readonly streamTimeout: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const streamTimeout: number;
|
|
23
|
+
/**
|
|
24
|
+
* `entry.server`'s `handleRequest` and `streamTimeout`, on the web stream
|
|
25
|
+
* renderer. Streams the shell, waits for the whole page for a bot and in
|
|
26
|
+
* SPA mode, answers HEAD with no body, stops rendering when the client goes
|
|
27
|
+
* away, and gives the router the response's CSP nonce.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createEntry(options?: EntryOptions): Entry;
|
|
30
|
+
/** The default entry: `export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry'`. */
|
|
31
|
+
export declare const handleRequest: HandleRequest;
|
|
32
|
+
//# sourceMappingURL=create-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-entry.d.ts","sourceRoot":"","sources":["../../src/entry/create-entry.ts"],"names":[],"mappings":"AAKA,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAE1B,MAAM,cAAc,CAAC;AAMtB,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EACZ,CAAC,CACD,WAAW,EAAE,qBAAqB,EAClC,OAAO,EAAE,OAAO,KACX,MAAM,GAAG,SAAS,CAAC,GACxB,SAAS,CAAC;IACb,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9D,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC1D;AAED,+DAA+D;AAC/D,MAAM,MAAM,aAAa,GAAG,CAC3B,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,OAAO,EACxB,aAAa,EAAE,YAAY,EAC3B,WAAW,EAAE,qBAAqB,KAC9B,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,qEAAqE;AACrE,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,aAAa,EAAE,MAAc,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,CAgE7D;AAID,gHAAgH;AAChH,eAAO,MAAM,aAAa,EAAE,aAAsC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@alxia/react-router/entry`: `entry.server.tsx` for a React Router app on
|
|
3
|
+
* Bun. Server only. It imports `react`, `react-dom/server`, `react-router`
|
|
4
|
+
* and, only when no `isBot` option is given, `isbot`: all optional peers but
|
|
5
|
+
* `react-router`.
|
|
6
|
+
*/
|
|
7
|
+
export { createEntry, type Entry, type EntryOptions, type HandleRequest, handleRequest, streamTimeout, } from './create-entry';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entry/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,WAAW,EACX,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,aAAa,EACb,aAAa,GACb,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentNonce2
|
|
3
|
+
} from "../chunks/index-y6vpnjr3.js";
|
|
4
|
+
import {
|
|
5
|
+
nonceOf2
|
|
6
|
+
} from "../chunks/index-5rt6hnhn.js";
|
|
7
|
+
|
|
8
|
+
// src/entry/create-entry.ts
|
|
9
|
+
import { createElement } from "react";
|
|
10
|
+
import {
|
|
11
|
+
renderToReadableStream
|
|
12
|
+
} from "react-dom/server";
|
|
13
|
+
import {
|
|
14
|
+
ServerRouter
|
|
15
|
+
} from "react-router";
|
|
16
|
+
|
|
17
|
+
// src/entry/abort.ts
|
|
18
|
+
function deadline(ms) {
|
|
19
|
+
const controller = new AbortController;
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
21
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) };
|
|
22
|
+
}
|
|
23
|
+
function untilDone(body, done) {
|
|
24
|
+
const reader = body.getReader();
|
|
25
|
+
return new ReadableStream({
|
|
26
|
+
async pull(controller) {
|
|
27
|
+
try {
|
|
28
|
+
const chunk = await reader.read();
|
|
29
|
+
if (chunk.done) {
|
|
30
|
+
done();
|
|
31
|
+
controller.close();
|
|
32
|
+
} else {
|
|
33
|
+
controller.enqueue(chunk.value);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
done();
|
|
37
|
+
controller.error(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async cancel(reason) {
|
|
41
|
+
done();
|
|
42
|
+
await reader.cancel(reason);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function abandon(limit, body) {
|
|
47
|
+
limit.clear();
|
|
48
|
+
await body?.cancel().catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/entry/whole-page.ts
|
|
52
|
+
var loaded;
|
|
53
|
+
function defaultIsBot() {
|
|
54
|
+
loaded ??= import("isbot").then((module) => module.isbot, (error) => {
|
|
55
|
+
loaded = undefined;
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
58
|
+
return loaded;
|
|
59
|
+
}
|
|
60
|
+
async function wantsWholePage(request, isSpaMode, isBot) {
|
|
61
|
+
if (isSpaMode)
|
|
62
|
+
return true;
|
|
63
|
+
const agent = request.headers.get("user-agent");
|
|
64
|
+
if (agent === null)
|
|
65
|
+
return false;
|
|
66
|
+
return (isBot ?? await defaultIsBot())(agent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/entry/create-entry.ts
|
|
70
|
+
var streamTimeout = 5000;
|
|
71
|
+
function createEntry(options = {}) {
|
|
72
|
+
const timeout = options.streamTimeout ?? streamTimeout;
|
|
73
|
+
const nonceFor = options.nonce ?? nonceOf2;
|
|
74
|
+
const report = options.onError ?? console.error;
|
|
75
|
+
const handleRequest = async (request, responseStatusCode, responseHeaders, routerContext, loadContext) => {
|
|
76
|
+
responseHeaders.set("Content-Type", "text/html");
|
|
77
|
+
if (request.method === "HEAD") {
|
|
78
|
+
return new Response(null, {
|
|
79
|
+
status: responseStatusCode,
|
|
80
|
+
headers: responseHeaders
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const nonce = nonceFor(loadContext, request);
|
|
84
|
+
const limit = deadline(timeout + 1000);
|
|
85
|
+
let status = responseStatusCode;
|
|
86
|
+
let shellOut = false;
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = await renderToReadableStream(createElement(DocumentNonce2, { nonce }, createElement(ServerRouter, {
|
|
90
|
+
context: routerContext,
|
|
91
|
+
url: request.url,
|
|
92
|
+
...nonce === undefined ? {} : { nonce }
|
|
93
|
+
})), {
|
|
94
|
+
nonce,
|
|
95
|
+
signal: AbortSignal.any([limit.signal, request.signal]),
|
|
96
|
+
onError(error) {
|
|
97
|
+
if (request.signal.aborted)
|
|
98
|
+
return;
|
|
99
|
+
status = 500;
|
|
100
|
+
if (shellOut)
|
|
101
|
+
report(error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
shellOut = true;
|
|
105
|
+
if (await wantsWholePage(request, routerContext.isSpaMode, options.isBot)) {
|
|
106
|
+
await body.allReady;
|
|
107
|
+
}
|
|
108
|
+
return new Response(untilDone(body, limit.clear), {
|
|
109
|
+
status,
|
|
110
|
+
headers: responseHeaders
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await abandon(limit, body);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return { handleRequest, streamTimeout: timeout };
|
|
118
|
+
}
|
|
119
|
+
var defaults = createEntry();
|
|
120
|
+
var handleRequest = defaults.handleRequest;
|
|
121
|
+
export {
|
|
122
|
+
createEntry,
|
|
123
|
+
handleRequest,
|
|
124
|
+
streamTimeout
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//# debugId=EB32BB9D687DF8D664756E2164756E21
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/entry/create-entry.ts", "../src/entry/abort.ts", "../src/entry/whole-page.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createElement } from 'react';\nimport {\n\ttype ReactDOMServerReadableStream,\n\trenderToReadableStream,\n} from 'react-dom/server';\nimport {\n\ttype EntryContext,\n\ttype RouterContextProvider,\n\tServerRouter,\n} from 'react-router';\nimport { DocumentNonce } from '../document';\nimport { nonceOf } from '../nonce';\nimport { abandon, deadline, untilDone } from './abort';\nimport { wantsWholePage } from './whole-page';\n\nexport interface EntryOptions {\n\t/**\n\t * Milliseconds React Router waits for a pending promise; the render itself\n\t * is aborted 1 000 ms later. Export it as `streamTimeout`. Default 5 000.\n\t */\n\treadonly streamTimeout?: number | undefined;\n\t/** This response's CSP nonce. Default `nonceOf(loadContext)`. */\n\treadonly nonce?:\n\t\t| ((\n\t\t\t\tloadContext: RouterContextProvider,\n\t\t\t\trequest: Request,\n\t\t ) => string | undefined)\n\t\t| undefined;\n\t/** Whether a user agent is a bot, which gets the whole page. Default `isbot`, which is then needed. */\n\treadonly isBot?: ((userAgent: string) => boolean) | undefined;\n\t/** An error after the shell is out. Default `console.error`. */\n\treadonly onError?: ((error: unknown) => void) | undefined;\n}\n\n/** React Router's `handleRequest` export of `entry.server`. */\nexport type HandleRequest = (\n\trequest: Request,\n\tresponseStatusCode: number,\n\tresponseHeaders: Headers,\n\trouterContext: EntryContext,\n\tloadContext: RouterContextProvider,\n) => Promise<Response>;\n\n/** What `createEntry` returns: the two exports of `entry.server`. */\nexport interface Entry {\n\treadonly handleRequest: HandleRequest;\n\treadonly streamTimeout: number;\n}\n\nexport const streamTimeout: number = 5_000;\n\n/**\n * `entry.server`'s `handleRequest` and `streamTimeout`, on the web stream\n * renderer. Streams the shell, waits for the whole page for a bot and in\n * SPA mode, answers HEAD with no body, stops rendering when the client goes\n * away, and gives the router the response's CSP nonce.\n */\nexport function createEntry(options: EntryOptions = {}): Entry {\n\tconst timeout = options.streamTimeout ?? streamTimeout;\n\tconst nonceFor = options.nonce ?? nonceOf;\n\tconst report = options.onError ?? console.error;\n\n\tconst handleRequest: HandleRequest = async (\n\t\trequest,\n\t\tresponseStatusCode,\n\t\tresponseHeaders,\n\t\trouterContext,\n\t\tloadContext,\n\t) => {\n\t\tresponseHeaders.set('Content-Type', 'text/html');\n\t\tif (request.method === 'HEAD') {\n\t\t\treturn new Response(null, {\n\t\t\t\tstatus: responseStatusCode,\n\t\t\t\theaders: responseHeaders,\n\t\t\t});\n\t\t}\n\t\tconst nonce = nonceFor(loadContext, request);\n\t\tconst limit = deadline(timeout + 1_000);\n\t\tlet status = responseStatusCode;\n\t\tlet shellOut = false;\n\t\tlet body: ReactDOMServerReadableStream | undefined;\n\t\ttry {\n\t\t\tbody = await renderToReadableStream(\n\t\t\t\tcreateElement(\n\t\t\t\t\tDocumentNonce,\n\t\t\t\t\t{ nonce },\n\t\t\t\t\tcreateElement(ServerRouter, {\n\t\t\t\t\t\tcontext: routerContext,\n\t\t\t\t\t\turl: request.url,\n\t\t\t\t\t\t// Under exactOptionalPropertyTypes, ServerRouter's nonce takes no undefined.\n\t\t\t\t\t\t...(nonce === undefined ? {} : { nonce }),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t\t{\n\t\t\t\t\tnonce,\n\t\t\t\t\tsignal: AbortSignal.any([limit.signal, request.signal]),\n\t\t\t\t\tonError(error: unknown) {\n\t\t\t\t\t\t// A client that left is not an error of ours.\n\t\t\t\t\t\tif (request.signal.aborted) return;\n\t\t\t\t\t\tstatus = 500;\n\t\t\t\t\t\tif (shellOut) report(error);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t\tshellOut = true;\n\t\t\tif (\n\t\t\t\tawait wantsWholePage(request, routerContext.isSpaMode, options.isBot)\n\t\t\t) {\n\t\t\t\tawait body.allReady;\n\t\t\t}\n\t\t\treturn new Response(untilDone(body, limit.clear), {\n\t\t\t\tstatus,\n\t\t\t\theaders: responseHeaders,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\t// A throwing isBot or a rejected allReady: stop the render.\n\t\t\tawait abandon(limit, body);\n\t\t\tthrow error;\n\t\t}\n\t};\n\treturn { handleRequest, streamTimeout: timeout };\n}\n\nconst defaults = createEntry();\n\n/** The default entry: `export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry'`. */\nexport const handleRequest: HandleRequest = defaults.handleRequest;\n",
|
|
6
|
+
"/** The render's abort timer: one per response, cleared when the response is over. */\nexport interface Deadline {\n\treadonly signal: AbortSignal;\n\t/** Stops the timer. Calling it again does nothing. */\n\treadonly clear: () => void;\n}\n\n/** A signal aborted after `ms`, with the means to stop the timer. */\nexport function deadline(ms: number): Deadline {\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(() => controller.abort(), ms);\n\treturn { signal: controller.signal, clear: () => clearTimeout(timer) };\n}\n\n/**\n * The same bytes in a new stream that calls `done` when the body ends,\n * fails or is cancelled (the client went away), so no timer outlives the\n * response. React's `allReady` is not carried over: await it first.\n */\nexport function untilDone(\n\tbody: ReadableStream<Uint8Array>,\n\tdone: () => void,\n): ReadableStream<Uint8Array> {\n\tconst reader = body.getReader();\n\treturn new ReadableStream<Uint8Array>({\n\t\tasync pull(controller) {\n\t\t\ttry {\n\t\t\t\tconst chunk = await reader.read();\n\t\t\t\tif (chunk.done) {\n\t\t\t\t\tdone();\n\t\t\t\t\tcontroller.close();\n\t\t\t\t} else {\n\t\t\t\t\tcontroller.enqueue(chunk.value);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tdone();\n\t\t\t\tcontroller.error(error);\n\t\t\t}\n\t\t},\n\t\tasync cancel(reason) {\n\t\t\tdone();\n\t\t\tawait reader.cancel(reason);\n\t\t},\n\t});\n}\n\n/**\n * Gives up on a render that will not be served: stops its timer and cancels\n * the body, which aborts React's work. A cancel that itself fails is ignored,\n * so the error that got us here is the one the caller rethrows.\n */\nexport async function abandon(\n\tlimit: Pick<Deadline, 'clear'>,\n\tbody: Pick<ReadableStream, 'cancel'> | undefined,\n): Promise<void> {\n\tlimit.clear();\n\tawait body?.cancel().catch(() => {});\n}\n",
|
|
7
|
+
"type IsBot = (userAgent: string) => boolean;\n\nlet loaded: Promise<IsBot> | undefined;\n\n/**\n * `isbot`, imported on the first bot check that needs it and kept: an\n * optional peer, so an app that gives `isBot` never loads it. A failed import\n * is not kept, so a later request tries again.\n */\nfunction defaultIsBot(): Promise<IsBot> {\n\tloaded ??= import('isbot').then(\n\t\t(module) => module.isbot,\n\t\t(error: unknown) => {\n\t\t\tloaded = undefined;\n\t\t\tthrow error;\n\t\t},\n\t);\n\treturn loaded;\n}\n\n/**\n * Whether the response waits for the whole page instead of streaming the\n * shell: a bot (`isBot`, `isbot` by default, imported only then) gets\n * complete HTML to index, and SPA mode renders the one shell the build\n * prerenders.\n */\nexport async function wantsWholePage(\n\trequest: Request,\n\tisSpaMode: boolean,\n\tisBot?: IsBot,\n): Promise<boolean> {\n\tif (isSpaMode) return true;\n\tconst agent = request.headers.get('user-agent');\n\tif (agent === null) return false;\n\treturn (isBot ?? (await defaultIsBot()))(agent);\n}\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "EB32BB9D687DF8D664756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type IsBot = (userAgent: string) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Whether the response waits for the whole page instead of streaming the
|
|
4
|
+
* shell: a bot (`isBot`, `isbot` by default, imported only then) gets
|
|
5
|
+
* complete HTML to index, and SPA mode renders the one shell the build
|
|
6
|
+
* prerenders.
|
|
7
|
+
*/
|
|
8
|
+
export declare function wantsWholePage(request: Request, isSpaMode: boolean, isBot?: IsBot): Promise<boolean>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=whole-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whole-page.d.ts","sourceRoot":"","sources":["../../src/entry/whole-page.ts"],"names":[],"mappings":"AAAA,KAAK,KAAK,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;AAoB5C;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,OAAO,EAClB,KAAK,CAAC,EAAE,KAAK,GACX,OAAO,CAAC,OAAO,CAAC,CAKlB"}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const value = context.get(alxiaContext);
|
|
7
|
-
if (value === MISSING) {
|
|
8
|
-
throw new Error("alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.");
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
Object.defineProperty(RouterContextProvider.prototype, "alxia", {
|
|
13
|
-
configurable: true,
|
|
14
|
-
enumerable: false,
|
|
15
|
-
get() {
|
|
16
|
-
return alxiaOf(this);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
// src/nonce.ts
|
|
20
|
-
function nonceOf(context) {
|
|
21
|
-
const value = context.get(alxiaContext);
|
|
22
|
-
if (typeof value !== "object" || value === null || !("nonce" in value)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
return typeof value.nonce === "string" ? value.nonce : undefined;
|
|
26
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
alxiaContext2,
|
|
3
|
+
alxiaOf2,
|
|
4
|
+
nonceOf2
|
|
5
|
+
} from "./chunks/index-5rt6hnhn.js";
|
|
27
6
|
// src/react-router.ts
|
|
28
7
|
import { fileURLToPath } from "node:url";
|
|
29
8
|
import {
|
|
@@ -31,7 +10,7 @@ import {
|
|
|
31
10
|
} from "@alxia/core";
|
|
32
11
|
import {
|
|
33
12
|
createRequestHandler,
|
|
34
|
-
RouterContextProvider
|
|
13
|
+
RouterContextProvider
|
|
35
14
|
} from "react-router";
|
|
36
15
|
|
|
37
16
|
// src/assets.ts
|
|
@@ -78,8 +57,8 @@ function reactRouter(app, options) {
|
|
|
78
57
|
const handle = requestHandler(options.build, mode);
|
|
79
58
|
const getLoadContext = options.getLoadContext;
|
|
80
59
|
const handler = async (ctx) => {
|
|
81
|
-
const context = new
|
|
82
|
-
context.set(
|
|
60
|
+
const context = new RouterContextProvider;
|
|
61
|
+
context.set(alxiaContext2, ctx);
|
|
83
62
|
await getLoadContext?.(ctx, context);
|
|
84
63
|
const response = await handle(routerRequest(ctx), context);
|
|
85
64
|
return ctx.reply(response.status, response.body ?? undefined, { headers: response.headers });
|
|
@@ -181,17 +160,17 @@ function announce({ dev, table, url }) {
|
|
|
181
160
|
}
|
|
182
161
|
// src/with-alxia.ts
|
|
183
162
|
function withAlxia(fn) {
|
|
184
|
-
return (args, ...rest) => fn({ ...args, alxia:
|
|
163
|
+
return (args, ...rest) => fn({ ...args, alxia: alxiaOf2(args.context) }, ...rest);
|
|
185
164
|
}
|
|
186
165
|
export {
|
|
187
|
-
alxiaContext,
|
|
188
|
-
alxiaOf,
|
|
166
|
+
alxiaContext2 as alxiaContext,
|
|
167
|
+
alxiaOf2 as alxiaOf,
|
|
189
168
|
createServer,
|
|
190
169
|
isReactRouterRoute,
|
|
191
|
-
nonceOf,
|
|
170
|
+
nonceOf2 as nonceOf,
|
|
192
171
|
reactRouter,
|
|
193
172
|
withAlxia
|
|
194
173
|
};
|
|
195
174
|
|
|
196
|
-
//# debugId=
|
|
175
|
+
//# debugId=121DFAE14BF4BF9464756E2164756E21
|
|
197
176
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/react-router.ts", "../src/assets.ts", "../src/server.ts", "../src/with-alxia.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * The key under which `reactRouter()` hands every loader, action and\n * middleware the request's alxia context, and the typed way to read it.\n */\nimport type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';\nimport { createContext, RouterContextProvider } from 'react-router';\nimport type { ReactRouterServer } from './server';\n\n/** The default of the key: no catch-all set it. */\nconst MISSING: unique symbol = Symbol('alxia context missing');\n\n/**\n * The key `reactRouter()` sets on React Router's context provider, on every\n * request, to what alxia's middlewares built for it. It lives in this package, so\n * it is one object whichever way the server was built or loaded: a key made\n * in the app's own `app/` folder is copied into React Router's build, and\n * the server that imports it separately sets a different one.\n */\nexport const alxiaContext = createContext<unknown>(MISSING);\n\n/**\n * Names the server whose context `alxiaOf(context)` reads when given no\n * type argument. One React Router build has one server, so it may be\n * declared once, beside it:\n *\n * ```ts\n * // app/server.ts\n * const server = createServer({ configure: (app) => app.use(session) });\n * export default server;\n *\n * declare module '@alxia/react-router' {\n * interface Register {\n * server: typeof server;\n * }\n * }\n * ```\n *\n * Unregistered here, `alxiaOf(context)` reads the app `@alxia/core`'s own\n * `Register` names as its `context`, and `BaseContext` when neither is\n * declared. When both are, this one wins: the server's app is the whole\n * app the pages run behind, the base's context and all `configure` adds.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: an app augments it\nexport interface Register {}\n\n/** The app a server makes, or the app itself. */\nexport type AppOf<Server> =\n\tServer extends ReactRouterServer<infer App> ? App : Server;\n\n/**\n * What `Register` names when it is neither a server nor an app — the\n * module, say, rather than its default export: an app whose context has\n * nothing but this key, so that reading anything of it is a compile error.\n */\nexport type InvalidRegister = Alxia<\n\t{\n\t\treadonly 'Register.server must be typeof server, the default export of createServer()': never;\n\t},\n\t''\n>;\n\n/**\n * The app `alxiaOf` reads for a `Register` interface: its server's, or\n * `InvalidRegister`; with no server, `Core`, the app `@alxia/core`'s\n * `Register` names (a fresh one when it names none).\n */\nexport type RegisteredOf<\n\tR,\n\tCore extends AnyAlxia = RegisteredBase,\n> = R extends {\n\treadonly server: infer Server;\n}\n\t? Server extends AnyAlxia | ReactRouterServer<AnyAlxia>\n\t\t? AppOf<Server>\n\t\t: InvalidRegister\n\t: Core;\n\n/** What `alxiaOf` reads with no type argument: the registered server's app, else core's registered app, else a fresh one. */\nexport type RegisteredApp = RegisteredOf<Register>;\n\n/**\n * What alxia's middlewares built for this request, read in a loader, an action or\n * a middleware, typed by the app: the server `Register` names, or the one\n * given as the type argument — `typeof server`, or an app *before* the\n * catch-all. With neither, the app `@alxia/core`'s `Register` names, and\n * `BaseContext` when nothing is registered.\n *\n * ```ts\n * export async function loader({ context }: Route.LoaderArgs) {\n * const { user, log } = alxiaOf(context);\n * }\n * ```\n *\n * Throws when the request did not come through `reactRouter()`: under\n * `react-router dev` without the plugin, or a test that calls a loader\n * directly.\n */\nexport function alxiaOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n>(context: ProviderLike): AlxiaContextOf<App> {\n\tconst value = context.get(alxiaContext);\n\tif (value === MISSING) {\n\t\tthrow new Error(\n\t\t\t\"alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.\",\n\t\t);\n\t}\n\treturn value as AlxiaContextOf<App>;\n}\n\n/**\n * What `alxiaOf` and `nonceOf` read of a provider: its `get`. Not the whole\n * class, which this package gives an `alxia`: the generated route types'\n * `context` is another declaration of it, without one, and must still pass.\n */\nexport type ProviderLike = Pick<Readonly<RouterContextProvider>, 'get'>;\n\n/** What `alxiaOf<App>(context)` returns, and `context.alxia` is for the registered app. */\nexport type AlxiaContextOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n> = ContextOf<AppOf<App>> & {\n\t/** The catch-all's route, which every request reaching React Router matched. */\n\treadonly route: string;\n};\n\ndeclare module 'react-router' {\n\tinterface RouterContextProvider {\n\t\t/**\n\t\t * What alxia's middlewares built for this request — `alxiaOf(context)`,\n\t\t * typed by the app `Register` names (`BaseContext` with none):\n\t\t *\n\t\t * ```ts\n\t\t * export async function loader({ context }: LoaderFunctionArgs) {\n\t\t * const { user, server } = context.alxia;\n\t\t * }\n\t\t * ```\n\t\t *\n\t\t * Typed with `react-router`'s own argument types. The generated\n\t\t * `Route.LoaderArgs` reads a second declaration of this class (its\n\t\t * `./internal` types point at react-router's development build), which\n\t\t * no augmentation reaches: read `alxiaOf(context)` there. Throws as\n\t\t * `alxiaOf` does on a request that did not come through\n\t\t * `reactRouter()`. Server-side only: a `clientLoader` has none.\n\t\t */\n\t\treadonly alxia: AlxiaContextOf;\n\t}\n}\n\n// A getter on the prototype rather than a value set on each provider: it\n// reads `alxiaContext`, so it holds exactly when that key is set — a\n// provider React Router copies keeps it — and on a provider no\n// `reactRouter()` filled, a test's, it throws `alxiaOf`'s error rather than\n// reading `undefined`.\nObject.defineProperty(RouterContextProvider.prototype, 'alxia', {\n\tconfigurable: true,\n\tenumerable: false,\n\tget(this: Readonly<RouterContextProvider>) {\n\t\treturn alxiaOf(this);\n\t},\n});\n",
|
|
6
|
-
"/** `nonceOf`: the request's CSP nonce, read from alxia's context if a middleware set one. */\nimport { alxiaContext, type ProviderLike } from './context';\n\n/**\n * This request's CSP nonce, for `entry.server.tsx`: the `nonce` alxia's\n * middlewares put on the context — `secureHeaders({ nonce: true })` from\n * `@alxia/secure-headers`, or a `derive` of your own — or `undefined` when\n * none did, or when the request did not come through alxia. It reads the\n * key if present: neither package depends on the other.\n *\n * ```tsx\n * const nonce = nonceOf(loadContext);\n * <ServerRouter context={routerContext} url={request.url} nonce={nonce} />\n * ```\n */\nexport function nonceOf(context: ProviderLike): string | undefined {\n\tconst value = context.get(alxiaContext);\n\tif (typeof value !== 'object' || value === null || !('nonce' in value)) {\n\t\treturn undefined;\n\t}\n\treturn typeof value.nonce === 'string' ? value.nonce : undefined;\n}\n",
|
|
7
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}\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}\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(app: AnyAlxia, client: string | URL): void {\n\tdeclaring(app, () => serveClient(asRoutes(app), pathOf(client)));\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",
|
|
8
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\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(app: Serving, client: string): void {\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)) app.static(route, path, { cacheControl });\n\t\t\telse app.file(route, path, { cacheControl });\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",
|
|
9
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 * `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);\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",
|
|
10
8
|
"/**\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"
|
|
11
9
|
],
|
|
12
|
-
"mappings": "
|
|
13
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "121DFAE14BF4BF9464756E2164756E21",
|
|
14
12
|
"names": []
|
|
15
13
|
}
|
package/dist/pwa/build/index.js
CHANGED
package/dist/pwa/index.js
CHANGED
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
cachingFor2,
|
|
9
9
|
isStorable2
|
|
10
10
|
} from "../chunks/index-zdbqzcjp.js";
|
|
11
|
+
import {
|
|
12
|
+
parseBrowserManifest2,
|
|
13
|
+
precacheList2,
|
|
14
|
+
assetPrecache2
|
|
15
|
+
} from "../chunks/index-5gqcpyyg.js";
|
|
11
16
|
import {
|
|
12
17
|
watchForUpdates2,
|
|
13
18
|
createUpdater2,
|
|
@@ -17,11 +22,6 @@ import {
|
|
|
17
22
|
createUpdateNotice2,
|
|
18
23
|
sessionStore2
|
|
19
24
|
} from "../chunks/index-9xv2a8et.js";
|
|
20
|
-
import {
|
|
21
|
-
parseBrowserManifest2,
|
|
22
|
-
precacheList2,
|
|
23
|
-
assetPrecache2
|
|
24
|
-
} from "../chunks/index-5gqcpyyg.js";
|
|
25
25
|
import {
|
|
26
26
|
SERVICE_WORKER_URL2,
|
|
27
27
|
OFFLINE_URL2,
|
|
@@ -61,5 +61,5 @@ export {
|
|
|
61
61
|
webAppManifest2 as webAppManifest
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
//# debugId=
|
|
64
|
+
//# debugId=C1B759B24638FB8364756E2164756E21
|
|
65
65
|
//# sourceMappingURL=index.js.map
|
package/dist/pwa/index.js.map
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"import type { WorkerContract } from './worker-contract';\n\n/**\n * Deletes every cache the service worker filled, but the offline page.\n * For sign-out: what the caches hold is static and public by design, and\n * clearing them leaves nothing behind anyway. Safe where Cache Storage does\n * not exist (server, old browsers).\n */\nexport async function clearAppCaches(\n\tcontract: Pick<WorkerContract, 'clearableCaches'>,\n): Promise<void> {\n\tif (typeof caches === 'undefined') return;\n\tconst names = contract.clearableCaches(await caches.keys());\n\tawait Promise.all(names.map((name) => caches.delete(name)));\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,eAAsB,cAAc,CACnC,UACgB;AAAA,EAChB,IAAI,OAAO,WAAW;AAAA,IAAa;AAAA,EACnC,MAAM,QAAQ,SAAS,gBAAgB,MAAM,OAAO,KAAK,CAAC;AAAA,EAC1D,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,OAAO,OAAO,IAAI,CAAC,CAAC;AAAA;",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "C1B759B24638FB8364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/vite/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;AAgBnD,OAAO,EAAE,KAAK,kBAAkB,EAAkB,MAAM,UAAU,CAAC;AAEnE,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,kBAAkB,CAAC;CACtD;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,MAAM,CA4ExD"}
|