gemi 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/{chunk-a0sy9f9x.js → chunk-07jtzbf0.js} +1 -1
  2. package/dist/{chunk-jdfe4dmh.js → chunk-1pqfmw5h.js} +2 -2
  3. package/dist/{chunk-jdfe4dmh.js.map → chunk-1pqfmw5h.js.map} +1 -1
  4. package/dist/chunk-3d3nmr5g.js +4 -0
  5. package/dist/{chunk-qj4mmner.js.map → chunk-3d3nmr5g.js.map} +2 -2
  6. package/dist/{chunk-et0j6jx8.js → chunk-7s1g3v1y.js} +3 -3
  7. package/dist/{chunk-et0j6jx8.js.map → chunk-7s1g3v1y.js.map} +1 -1
  8. package/dist/chunk-92793zh4.js +5 -0
  9. package/dist/{chunk-0tbwn6er.js.map → chunk-92793zh4.js.map} +3 -4
  10. package/dist/chunk-9279y4af.js +5 -0
  11. package/dist/{chunk-f29vd5mc.js.map → chunk-9279y4af.js.map} +2 -2
  12. package/dist/chunk-bq4w14a6.js +5 -0
  13. package/dist/chunk-bq4w14a6.js.map +10 -0
  14. package/dist/{chunk-4t6a82fh.js → chunk-cg53h5ht.js} +2 -2
  15. package/dist/{chunk-4t6a82fh.js.map → chunk-cg53h5ht.js.map} +1 -1
  16. package/dist/{chunk-km6x37ws.js → chunk-ear03axe.js} +24 -24
  17. package/dist/{chunk-km6x37ws.js.map → chunk-ear03axe.js.map} +3 -5
  18. package/dist/{chunk-38ma6gg3.js → chunk-fbp5vapa.js} +2 -2
  19. package/dist/{chunk-38ma6gg3.js.map → chunk-fbp5vapa.js.map} +2 -2
  20. package/dist/chunk-ffre1z8s.js +6 -0
  21. package/dist/{chunk-hbycemy5.js.map → chunk-ffre1z8s.js.map} +2 -2
  22. package/dist/{chunk-53e48ce8.js → chunk-qjnv2rkh.js} +2 -2
  23. package/dist/{chunk-53e48ce8.js.map → chunk-qjnv2rkh.js.map} +1 -1
  24. package/dist/{chunk-1ce7veh9.js → chunk-tkv6cw94.js} +3 -3
  25. package/dist/{chunk-1ce7veh9.js.map → chunk-tkv6cw94.js.map} +1 -1
  26. package/dist/chunk-y4djde42.js +5 -0
  27. package/dist/chunk-y4djde42.js.map +11 -0
  28. package/dist/chunks/ThemeProvider-CTX9LOFC.js +2718 -0
  29. package/dist/chunks/ThemeProvider-CTX9LOFC.js.map +1 -0
  30. package/dist/client/Mutation.d.ts +1 -1
  31. package/dist/client/Mutation.d.ts.map +1 -1
  32. package/dist/client/ThemeProvider.d.ts +7 -0
  33. package/dist/client/ThemeProvider.d.ts.map +1 -1
  34. package/dist/client/index.js +107 -2673
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/useIsNavigationPending.d.ts.map +1 -1
  37. package/dist/client/useMutate.d.ts.map +1 -1
  38. package/dist/database/index.js +2 -2
  39. package/dist/database/index.js.map +1 -1
  40. package/dist/facades/index.js +2 -2
  41. package/dist/facades/index.js.map +1 -1
  42. package/dist/http/index.js +2 -2
  43. package/dist/http/index.js.map +1 -1
  44. package/dist/i18n/Dictionary.d.ts.map +1 -1
  45. package/dist/i18n/index.js +2 -2
  46. package/dist/i18n/index.js.map +3 -3
  47. package/dist/kernel/index.js +2 -2
  48. package/dist/kernel/index.js.map +2 -2
  49. package/dist/orm/index.js +2 -2
  50. package/dist/orm/index.js.map +2 -2
  51. package/dist/server/index.js +1 -1
  52. package/dist/services/index.js +2 -2
  53. package/dist/services/index.js.map +2 -2
  54. package/dist/testing/Page.d.ts +166 -0
  55. package/dist/testing/Page.d.ts.map +1 -0
  56. package/dist/testing/index.d.ts +3 -0
  57. package/dist/testing/index.d.ts.map +1 -0
  58. package/dist/testing/index.js +352 -0
  59. package/dist/testing/index.js.map +1 -0
  60. package/dist/utils/Subject.d.ts.map +1 -1
  61. package/dist/utils/variantKey.d.ts +19 -0
  62. package/dist/utils/variantKey.d.ts.map +1 -0
  63. package/package.json +2 -1
  64. package/dist/chunk-0tbwn6er.js +0 -5
  65. package/dist/chunk-f29vd5mc.js +0 -5
  66. package/dist/chunk-hbycemy5.js +0 -6
  67. package/dist/chunk-qj4mmner.js +0 -4
  68. /package/dist/{chunk-a0sy9f9x.js.map → chunk-07jtzbf0.js.map} +0 -0
@@ -0,0 +1,166 @@
1
+ import { type ComponentType, type PropsWithChildren, type ReactNode } from "react";
2
+ import { type FallbackProps } from "react-error-boundary";
3
+ import type { User } from "../auth/types";
4
+ import { type QueryConfig } from "../client/QueryManagerContext";
5
+ import type { Breadcrumb } from "../client/useBreadcrumbs";
6
+ /**
7
+ * A dictionary as `Dictionary.create` produces it: a name and a
8
+ * `{ key: { locale: string } }` map.
9
+ *
10
+ * Typed structurally rather than as `Dictionary<T>` for two reasons. It keeps
11
+ * `gemi/testing` from importing `gemi/i18n` — a server entry, which reaches the
12
+ * `Lang` facade and through it the container. And it means an object literal of
13
+ * the same shape is accepted, so a test never *has* to import the app's
14
+ * dictionaries at all.
15
+ *
16
+ * Only `name` and `dictionary` are read — both plain data. `Dictionary`'s
17
+ * server-only methods (`render`, `reference`, which throw once `window` is
18
+ * defined) are never called from here.
19
+ */
20
+ export interface PageDictionary {
21
+ name: string;
22
+ dictionary: Record<string, Record<string, string>>;
23
+ }
24
+ export interface PageProps {
25
+ /**
26
+ * The URL the view is mounted at. A template (`/app/:orgId/chat`) is resolved
27
+ * against `params`, so the same string can be pasted from the router.
28
+ */
29
+ pathname?: string;
30
+ /** Route params, as the router would have parsed them out of `pathname`. */
31
+ params?: Record<string, string>;
32
+ /** `"?tab=recent"`, `"tab=recent"`, or `{ tab: "recent" }`. */
33
+ searchParams?: string | Record<string, string | number | boolean>;
34
+ hash?: string;
35
+ /** The locale `useTranslator` and `useLocale` report. */
36
+ locale?: string;
37
+ /**
38
+ * The app's default locale. Links and `useNavigate` omit the locale segment
39
+ * for it, so leaving this equal to `locale` (the default) keeps hrefs
40
+ * unprefixed — set it to seed a page being viewed in a non-default locale.
41
+ */
42
+ defaultLocale?: string;
43
+ supportedLocales?: string[];
44
+ /**
45
+ * Dictionaries the components under test translate against — the app's own,
46
+ * or literals of the same shape.
47
+ *
48
+ * Note what importing the app's costs: `app/i18n/index.ts` imports
49
+ * `gemi/i18n`, a server entry, so the test file pulls the container and
50
+ * `node:async_hooks` into its module graph. Nothing there runs (and nothing
51
+ * here calls it), so it is fine under any runner that has Node builtins —
52
+ * vitest, `bun test` — and not under one that renders in a real browser. Use
53
+ * `translations` there, which is the shape the client actually receives.
54
+ */
55
+ dictionaries?: PageDictionary[];
56
+ /**
57
+ * Translations for the current locale, already resolved: dictionary name →
58
+ * key → string. This is the shape the server serializes onto the page, so it
59
+ * is what the client reads at runtime — and it imports nothing.
60
+ *
61
+ * ```tsx
62
+ * <Page translations={{ Chat: { greeting: "Hello {{name}}" } }}>
63
+ * ```
64
+ *
65
+ * Merged over `dictionaries`, so the two compose: seed from the app's real
66
+ * dictionary and override the one key a test is about.
67
+ */
68
+ translations?: Record<string, Record<string, string>>;
69
+ /**
70
+ * Data `useQuery` finds already cached, keyed by API path — the path passed
71
+ * to `useQuery`, without the `/api` prefix the client adds when it fetches.
72
+ * A key may carry a query string (`"/lists?page=2"`) to seed one search
73
+ * variant.
74
+ *
75
+ * The cache is keyed by the *resolved* path, so a key's `:params` are
76
+ * resolved against the page's `params` — and a key naming one the page does
77
+ * not carry is an error, naming the key. That case is common enough to be
78
+ * worth the loud failure: a query often takes its params from the call site
79
+ * rather than the route (`useQuery("/orgs/:orgId/lists", { params: { orgId } })`
80
+ * with `orgId` in component state), and the right seed for it is the resolved
81
+ * path, `"/orgs/abc/lists"`.
82
+ *
83
+ * Seeded at mount only: a component that mutates or refetches its data owns
84
+ * the cache from then on.
85
+ */
86
+ queryData?: Record<string, unknown>;
87
+ /** App-wide `useQuery` defaults, as `createRoot` threads them. */
88
+ queryConfig?: QueryConfig;
89
+ /**
90
+ * What `useUser()` reports. Defaults to `null` — an anonymous visitor — and
91
+ * either way the hook resolves from here without issuing a request.
92
+ */
93
+ user?: Partial<User> | null;
94
+ /** What `useBreadcrumbs()` returns, in order. */
95
+ breadcrumbs?: Breadcrumb[];
96
+ /**
97
+ * The theme `useTheme()` starts on. The app reads a visitor's stored choice
98
+ * out of `localStorage`; a test has no session to have made one in, so this
99
+ * seeds it without writing to storage. `setTheme` works from either.
100
+ */
101
+ theme?: "light" | "dark" | "system";
102
+ /**
103
+ * Fallback for the `Suspense` boundary wrapped around the children — the
104
+ * stand-in for the view's own `Loading` export, which the real router
105
+ * supplies. Defaults to `null`.
106
+ */
107
+ fallback?: ReactNode;
108
+ /**
109
+ * Renders in place of the children when one throws, mirroring a view's
110
+ * `Error` export. Without it a thrown render (an errored suspense query,
111
+ * say) propagates out of `render()` and fails the test, which is usually
112
+ * what you want.
113
+ */
114
+ errorFallback?: ComponentType<FallbackProps>;
115
+ /**
116
+ * Called when something navigates — a `Link` click, `useNavigate().push`.
117
+ * The navigation is reported, not performed: `<Page>` renders one route and
118
+ * does not re-resolve the tree, so `useLocation()` keeps reporting the
119
+ * pathname it was given.
120
+ */
121
+ onNavigate?: (href: string, action: "push" | "replace") => void;
122
+ }
123
+ /**
124
+ * Mounts a component with the inputs a view normally arrives with — route
125
+ * params, the current locale and its dictionaries, prefetched query data, the
126
+ * signed-in user — so a unit test can assert what it renders rather than only
127
+ * its empty state.
128
+ *
129
+ * ```tsx
130
+ * import { render, screen } from "@testing-library/react";
131
+ * import { Page } from "gemi/testing";
132
+ *
133
+ * render(
134
+ * <Page
135
+ * pathname="/app/:orgId/chat"
136
+ * params={{ orgId: "abc" }}
137
+ * queryData={{ "/organizations/:orgId/messages": [{ id: 1, body: "hi" }] }}
138
+ * dictionaries={[ChatDictionary]}
139
+ * >
140
+ * <OrgChat />
141
+ * </Page>,
142
+ * );
143
+ *
144
+ * expect(screen.getByText("hi")).toBeDefined();
145
+ * ```
146
+ *
147
+ * It is a plain component, so it composes with any renderer — Testing
148
+ * Library's `render`, `react-test-renderer`, or `renderToString`.
149
+ *
150
+ * Two things it does not do.
151
+ *
152
+ * It does not route: there is no view tree and no route manifest behind it, so
153
+ * a navigation is reported through `onNavigate` rather than resolved. To
154
+ * assert the page *after* one, render a second `<Page>`.
155
+ *
156
+ * And under `renderToString` it renders the server's own no-data branch for an
157
+ * *unseeded* query rather than suspending — deliberately, because suspending
158
+ * there is the streaming server's job: `createRoot` threads the request's
159
+ * `ServerQueryStore` through `ServerQueryContext`, and that store is what runs
160
+ * the route handler. A harness has no request and no handler to run, so there
161
+ * is nothing to suspend on. Seeded queries render their data on the server
162
+ * exactly as they do in the browser; unseeded ones behave as a route with no
163
+ * `Query.prefetch` does, framework warning included.
164
+ */
165
+ export declare const Page: (props: PropsWithChildren<PageProps>) => import("react/jsx-runtime").JSX.Element;
166
+ //# sourceMappingURL=Page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Page.d.ts","sourceRoot":"","sources":["../../testing/Page.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAI1C,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAYvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAM3D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,kEAAkE;IAClE,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CACjE;AA6ID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,iBAAiB,CAAC,SAAS,CAAC,4CA4PvD,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { Page } from "./Page";
2
+ export type { PageProps, PageDictionary } from "./Page";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC"}
@@ -0,0 +1,352 @@
1
+ import { A as toVariantKey, E as createMemoryHistory, L as Subject, N as QueryManagerContext, P as QueryManagerProvider, T as Action, a as m, b as I18nProvider, c as ClientRouterContext, j as applyParams, k as RouteStateProvider, o as RouteTransitionProvider, r as WebSocketContext, t as ThemeProvider, w as ProgressManager, x as ServerDataContext } from "../chunks/ThemeProvider-CTX9LOFC.js";
2
+ import { Suspense, useContext, useMemo, useRef, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region testing/Page.tsx
5
+ /** `"?a=b"` (or `""`) from any of the shapes `searchParams` accepts. */
6
+ function toSearchString(search) {
7
+ if (!search) return "";
8
+ const query = typeof search === "string" ? search.replace(/^\?/, "") : new URLSearchParams(Object.entries(search).map(([key, value]) => [key, String(value)])).toString();
9
+ return query ? `?${query}` : "";
10
+ }
11
+ /**
12
+ * The `:params` a seed key needs supplied. Wildcards (`:rest*`) are excluded —
13
+ * `applyParams` drops those when missing, which is a resolution rather than a
14
+ * failure.
15
+ *
16
+ * Optional params (`:id?`) cannot reach here at all: a key is split at its
17
+ * first `?` to separate the search string, so `"/lists/:folderId?"` arrives as
18
+ * the path `/lists/:folderId`. Seed the resolved path for those.
19
+ */
20
+ function requiredParamsOf(path) {
21
+ return Array.from(path.matchAll(/:([^/]+)/g), ([, name]) => name).filter((name) => !name.endsWith("*"));
22
+ }
23
+ /**
24
+ * Fails a `queryData` key whose `:params` the page does not carry, saying so
25
+ * in terms of the key.
26
+ *
27
+ * Without this the two documented runners disagree, and neither is legible.
28
+ * `applyParams` throws `Missing parameter: orgId` under `import.meta.env.DEV`
29
+ * (vitest) — a router-utility stack trace out of `<Page>`'s render, before
30
+ * anything mounts, naming nothing that appears in the test. Under `bun test`
31
+ * that flag is falsy, so it logs and seeds the literal `/orgs/undefined/lists`
32
+ * instead: the seed misses, the query goes to the network, and the assertion
33
+ * runs against a loading state.
34
+ *
35
+ * The case is common because a query's params often come from the call site
36
+ * rather than the route — `useQuery("/orgs/:orgId/lists", { params: { orgId } })`
37
+ * with `orgId` in component state. The cache is keyed by the *resolved* path,
38
+ * so the way out is to seed that path directly.
39
+ */
40
+ function resolveSeedPath(key, rawPath, params) {
41
+ const missing = requiredParamsOf(rawPath).filter((name) => params[name] === void 0);
42
+ if (missing.length > 0) throw new Error(`[gemi] <Page> cannot resolve the queryData key "${key}": no value for ${missing.map((name) => `:${name}`).join(", ")}. The query cache is keyed by the resolved path, so either add ${missing.map((name) => `\`${name}\``).join(", ")} to the page's \`params\`, or seed the resolved path (e.g. "${applyParams(rawPath, Object.fromEntries(missing.map((name) => [name, "123"])))}") — which is what a query taking its params from component state reads under.`);
43
+ return applyParams(rawPath, params);
44
+ }
45
+ /**
46
+ * `queryData` in the shape the query cache seeds from: `{ [path]: { [variant]:
47
+ * data } }`, the same payload `Query.prefetch` puts on a server-rendered page.
48
+ */
49
+ function toPrefetchedData(queryData, params, warned) {
50
+ const prefetchedData = {};
51
+ for (const [key, data] of Object.entries(queryData)) {
52
+ const [rawPath, rawSearch = ""] = key.split("?");
53
+ if (rawPath.startsWith("/api/") && !warned.has(key)) {
54
+ warned.add(key);
55
+ console.warn(`[gemi] queryData key "${key}" starts with /api. useQuery is keyed by the path you pass it — the /api prefix is added when it fetches — so this entry seeds a path no query reads.`);
56
+ }
57
+ const path = resolveSeedPath(key, rawPath, params);
58
+ prefetchedData[path] = {
59
+ ...prefetchedData[path],
60
+ [toVariantKey(rawSearch)]: data
61
+ };
62
+ }
63
+ return prefetchedData;
64
+ }
65
+ /**
66
+ * `{ [locale]: { [dictionary]: { [key]: string } } }` — the payload
67
+ * `Translator` serializes onto the page, which is the only form the client
68
+ * ever sees. `dictionaries` are transposed into it (they are keyed the other
69
+ * way round, by key then locale); `translations` is already in it and lands
70
+ * under `locale`, last, so it wins.
71
+ */
72
+ function toClientDictionary(dictionaries, locales, locale, translations) {
73
+ const clientDictionary = {};
74
+ for (const supported of locales) clientDictionary[supported] ??= {};
75
+ for (const { name, dictionary } of dictionaries) for (const [key, byLocale] of Object.entries(dictionary ?? {})) for (const [dictLocale, translation] of Object.entries(byLocale ?? {})) {
76
+ clientDictionary[dictLocale] ??= {};
77
+ clientDictionary[dictLocale][name] ??= {};
78
+ clientDictionary[dictLocale][name][key] = translation;
79
+ }
80
+ for (const [name, keys] of Object.entries(translations)) {
81
+ clientDictionary[locale] ??= {};
82
+ clientDictionary[locale][name] = {
83
+ ...clientDictionary[locale][name],
84
+ ...keys
85
+ };
86
+ }
87
+ return clientDictionary;
88
+ }
89
+ /**
90
+ * Mounts a component with the inputs a view normally arrives with — route
91
+ * params, the current locale and its dictionaries, prefetched query data, the
92
+ * signed-in user — so a unit test can assert what it renders rather than only
93
+ * its empty state.
94
+ *
95
+ * ```tsx
96
+ * import { render, screen } from "@testing-library/react";
97
+ * import { Page } from "gemi/testing";
98
+ *
99
+ * render(
100
+ * <Page
101
+ * pathname="/app/:orgId/chat"
102
+ * params={{ orgId: "abc" }}
103
+ * queryData={{ "/organizations/:orgId/messages": [{ id: 1, body: "hi" }] }}
104
+ * dictionaries={[ChatDictionary]}
105
+ * >
106
+ * <OrgChat />
107
+ * </Page>,
108
+ * );
109
+ *
110
+ * expect(screen.getByText("hi")).toBeDefined();
111
+ * ```
112
+ *
113
+ * It is a plain component, so it composes with any renderer — Testing
114
+ * Library's `render`, `react-test-renderer`, or `renderToString`.
115
+ *
116
+ * Two things it does not do.
117
+ *
118
+ * It does not route: there is no view tree and no route manifest behind it, so
119
+ * a navigation is reported through `onNavigate` rather than resolved. To
120
+ * assert the page *after* one, render a second `<Page>`.
121
+ *
122
+ * And under `renderToString` it renders the server's own no-data branch for an
123
+ * *unseeded* query rather than suspending — deliberately, because suspending
124
+ * there is the streaming server's job: `createRoot` threads the request's
125
+ * `ServerQueryStore` through `ServerQueryContext`, and that store is what runs
126
+ * the route handler. A harness has no request and no handler to run, so there
127
+ * is nothing to suspend on. Seeded queries render their data on the server
128
+ * exactly as they do in the browser; unseeded ones behave as a route with no
129
+ * `Query.prefetch` does, framework warning included.
130
+ */
131
+ var Page = (props) => {
132
+ const { children, pathname: routePath = "/", params = {}, searchParams, hash = "", locale = "en-US", defaultLocale = locale, dictionaries = [], translations = {}, queryData = {}, queryConfig, user = null, breadcrumbs = [], theme, fallback = null, errorFallback, onNavigate } = props;
133
+ const supportedLocales = useMemo(() => Array.from(/* @__PURE__ */ new Set([
134
+ ...props.supportedLocales ?? [],
135
+ defaultLocale,
136
+ locale
137
+ ])), [
138
+ props.supportedLocales,
139
+ defaultLocale,
140
+ locale
141
+ ]);
142
+ const pathname = applyParams(routePath, params) || "/";
143
+ const search = toSearchString(searchParams);
144
+ const urlLocaleSegment = locale === defaultLocale ? null : locale;
145
+ const dictionary = useMemo(() => toClientDictionary(dictionaries, supportedLocales, locale, translations), [
146
+ dictionaries,
147
+ supportedLocales,
148
+ locale,
149
+ translations
150
+ ]);
151
+ const warnedRef = useRef(/* @__PURE__ */ new Set());
152
+ const prefetchedData = useMemo(() => {
153
+ const seeded = toPrefetchedData(queryData, params, warnedRef.current);
154
+ seeded["/auth/me"] ??= { "": user ?? null };
155
+ return seeded;
156
+ }, [
157
+ queryData,
158
+ params,
159
+ user
160
+ ]);
161
+ const { breadcrumbViews, breadcrumbsCache, breadcrumbsRecord } = useMemo(() => {
162
+ const views = breadcrumbs.map((_, index) => `breadcrumb-${index}`);
163
+ const cache = new Map(breadcrumbs.map((breadcrumb, index) => [`${views[index]}:${pathname}`, breadcrumb]));
164
+ return {
165
+ breadcrumbViews: views,
166
+ breadcrumbsCache: cache,
167
+ breadcrumbsRecord: Object.fromEntries(cache)
168
+ };
169
+ }, [breadcrumbs, pathname]);
170
+ const [isNavigatingSubject] = useState(() => new Subject(false));
171
+ const [progressManager] = useState(() => new ProgressManager(isNavigatingSubject));
172
+ const [history] = useState(() => createMemoryHistory({ initialEntries: [`${pathname}${search}${hash}`] }));
173
+ const routeState = useMemo(() => ({
174
+ views: [],
175
+ params,
176
+ search,
177
+ state: {},
178
+ pathname,
179
+ hash,
180
+ action: null,
181
+ routePath,
182
+ locale: urlLocaleSegment,
183
+ data: {},
184
+ i18n: {
185
+ currentLocale: locale,
186
+ dictionary,
187
+ supportedLocales
188
+ },
189
+ prefetchedData,
190
+ breadcrumbs: breadcrumbsRecord,
191
+ appId: "test"
192
+ }), [
193
+ params,
194
+ search,
195
+ pathname,
196
+ hash,
197
+ routePath,
198
+ urlLocaleSegment,
199
+ locale,
200
+ dictionary,
201
+ supportedLocales,
202
+ prefetchedData,
203
+ breadcrumbsRecord
204
+ ]);
205
+ const [routerSubject] = useState(() => new Subject(routeState));
206
+ const onNavigateRef = useRef(onNavigate);
207
+ onNavigateRef.current = onNavigate;
208
+ const listeningRef = useRef(false);
209
+ if (!listeningRef.current) {
210
+ listeningRef.current = true;
211
+ history.listen(({ location, action }) => {
212
+ const href = `${location.pathname}${location.search}${location.hash}`;
213
+ onNavigateRef.current?.(href, action === Action.Replace ? "replace" : "push");
214
+ });
215
+ }
216
+ const serverData = useMemo(() => ({
217
+ routeManifest: {},
218
+ pageData: {},
219
+ breadcrumbs: breadcrumbsRecord,
220
+ prefetchedData,
221
+ router: {
222
+ pathname,
223
+ params,
224
+ currentPath: routePath,
225
+ is404: false,
226
+ searchParams: search,
227
+ urlLocaleSegment
228
+ },
229
+ i18n: {
230
+ dictionary,
231
+ currentLocale: locale,
232
+ supportedLocales,
233
+ defaultLocale
234
+ },
235
+ componentTree: [],
236
+ auth: { user },
237
+ __csrf: "test-csrf-token",
238
+ cssManifest: {},
239
+ modulePreloadManifest: {},
240
+ meta: {},
241
+ appId: "test"
242
+ }), [
243
+ breadcrumbsRecord,
244
+ prefetchedData,
245
+ pathname,
246
+ params,
247
+ routePath,
248
+ search,
249
+ urlLocaleSegment,
250
+ dictionary,
251
+ locale,
252
+ supportedLocales,
253
+ defaultLocale,
254
+ user
255
+ ]);
256
+ const [viewEntriesSubject] = useState(() => new Subject([]));
257
+ const routerContext = useMemo(() => ({
258
+ viewEntriesSubject,
259
+ history,
260
+ updatePageData: () => {},
261
+ getPageData: () => ({}),
262
+ getScrollPosition: () => 0,
263
+ getViewPathsFromPathname: () => breadcrumbViews,
264
+ getRoutePathnameFromHref: (href) => href,
265
+ isNavigatingSubject,
266
+ setNavigationAbortController: () => {},
267
+ progressManager,
268
+ fetchRouteCSS: async () => {},
269
+ preloadRouteModules: () => {},
270
+ prefetchRoute: async () => {},
271
+ takePrefetched: () => null,
272
+ clearPrefetchCache: () => {},
273
+ breadcrumbsCache,
274
+ routerSubject,
275
+ urlLocaleSegment
276
+ }), [
277
+ viewEntriesSubject,
278
+ history,
279
+ breadcrumbViews,
280
+ isNavigatingSubject,
281
+ progressManager,
282
+ breadcrumbsCache,
283
+ routerSubject,
284
+ urlLocaleSegment
285
+ ]);
286
+ const [websocket] = useState(() => ({
287
+ subscribe: async () => {},
288
+ unsubscribe: async () => {},
289
+ broadcast: () => {}
290
+ }));
291
+ return /* @__PURE__ */ jsx(ThemeProvider, {
292
+ theme,
293
+ children: /* @__PURE__ */ jsx(ServerDataContext.Provider, {
294
+ value: serverData,
295
+ children: /* @__PURE__ */ jsx(I18nProvider, { children: /* @__PURE__ */ jsx(WebSocketContext.Provider, {
296
+ value: websocket,
297
+ children: /* @__PURE__ */ jsx(QueryManagerProvider, {
298
+ queryConfig,
299
+ children: /* @__PURE__ */ jsx(ClientRouterContext.Provider, {
300
+ value: routerContext,
301
+ children: /* @__PURE__ */ jsx(RouteTransitionProvider, {
302
+ isPending: false,
303
+ isFetching: false,
304
+ transitionPath: ["", pathname],
305
+ children: /* @__PURE__ */ jsx(RouteStateProvider, {
306
+ state: routeState,
307
+ children: /* @__PURE__ */ jsx(Boundary, {
308
+ errorFallback,
309
+ fallback,
310
+ resetKey: pathname,
311
+ children
312
+ })
313
+ })
314
+ })
315
+ })
316
+ })
317
+ }) })
318
+ })
319
+ });
320
+ };
321
+ /**
322
+ * The `Suspense` boundary every view renders inside, and — when the test
323
+ * supplies a fallback for it — the error boundary too.
324
+ *
325
+ * `onReset={clearErrors}` is the part that has to be here rather than inline
326
+ * above: it needs `QueryManagerContext`, and it is what makes a view's real
327
+ * `Error` export testable. A fallback whose "Try again" calls
328
+ * `resetErrorBoundary()` re-renders the child, and `useQuery` reads the
329
+ * failure still stored on the `QueryResource` and throws it straight back —
330
+ * so without this the fallback never goes away and the retry path that works
331
+ * in the app cannot be exercised. `ClientRouter`'s own boundary wires exactly
332
+ * this pair.
333
+ */
334
+ var Boundary = (props) => {
335
+ const { errorFallback: ErrorFallback, fallback, resetKey, children } = props;
336
+ const { clearErrors } = useContext(QueryManagerContext);
337
+ const content = /* @__PURE__ */ jsx(Suspense, {
338
+ fallback,
339
+ children
340
+ });
341
+ if (!ErrorFallback) return content;
342
+ return /* @__PURE__ */ jsx(m, {
343
+ FallbackComponent: ErrorFallback,
344
+ resetKeys: [resetKey],
345
+ onReset: clearErrors,
346
+ children: content
347
+ });
348
+ };
349
+ //#endregion
350
+ export { Page };
351
+
352
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../testing/Page.tsx"],"sourcesContent":["import {\n Suspense,\n useContext,\n useMemo,\n useRef,\n useState,\n type ComponentType,\n type PropsWithChildren,\n type ReactNode,\n} from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\nimport { Action, createMemoryHistory } from \"history\";\n\nimport type { User } from \"../auth/types\";\nimport { ClientRouterContext } from \"../client/ClientRouterContext\";\nimport { I18nProvider } from \"../client/I18nContext\";\nimport { ProgressManager } from \"../client/ProgressManager\";\nimport {\n QueryManagerContext,\n QueryManagerProvider,\n type QueryConfig,\n} from \"../client/QueryManagerContext\";\nimport {\n RouteStateProvider,\n type PageData,\n type RouteState,\n} from \"../client/RouteStateContext\";\nimport { RouteTransitionProvider } from \"../client/RouteTransitionProvider\";\nimport {\n ServerDataContext,\n type ServerDataContextValue,\n} from \"../client/ServerDataProvider\";\nimport { ThemeProvider } from \"../client/ThemeProvider\";\nimport type { Breadcrumb } from \"../client/useBreadcrumbs\";\nimport { WebSocketContext } from \"../client/WebsocketContext\";\nimport { applyParams } from \"../utils/applyParams\";\nimport { Subject } from \"../utils/Subject\";\nimport { toVariantKey } from \"../utils/variantKey\";\n\n/**\n * A dictionary as `Dictionary.create` produces it: a name and a\n * `{ key: { locale: string } }` map.\n *\n * Typed structurally rather than as `Dictionary<T>` for two reasons. It keeps\n * `gemi/testing` from importing `gemi/i18n` — a server entry, which reaches the\n * `Lang` facade and through it the container. And it means an object literal of\n * the same shape is accepted, so a test never *has* to import the app's\n * dictionaries at all.\n *\n * Only `name` and `dictionary` are read — both plain data. `Dictionary`'s\n * server-only methods (`render`, `reference`, which throw once `window` is\n * defined) are never called from here.\n */\nexport interface PageDictionary {\n name: string;\n dictionary: Record<string, Record<string, string>>;\n}\n\nexport interface PageProps {\n /**\n * The URL the view is mounted at. A template (`/app/:orgId/chat`) is resolved\n * against `params`, so the same string can be pasted from the router.\n */\n pathname?: string;\n /** Route params, as the router would have parsed them out of `pathname`. */\n params?: Record<string, string>;\n /** `\"?tab=recent\"`, `\"tab=recent\"`, or `{ tab: \"recent\" }`. */\n searchParams?: string | Record<string, string | number | boolean>;\n hash?: string;\n /** The locale `useTranslator` and `useLocale` report. */\n locale?: string;\n /**\n * The app's default locale. Links and `useNavigate` omit the locale segment\n * for it, so leaving this equal to `locale` (the default) keeps hrefs\n * unprefixed — set it to seed a page being viewed in a non-default locale.\n */\n defaultLocale?: string;\n supportedLocales?: string[];\n /**\n * Dictionaries the components under test translate against — the app's own,\n * or literals of the same shape.\n *\n * Note what importing the app's costs: `app/i18n/index.ts` imports\n * `gemi/i18n`, a server entry, so the test file pulls the container and\n * `node:async_hooks` into its module graph. Nothing there runs (and nothing\n * here calls it), so it is fine under any runner that has Node builtins —\n * vitest, `bun test` — and not under one that renders in a real browser. Use\n * `translations` there, which is the shape the client actually receives.\n */\n dictionaries?: PageDictionary[];\n /**\n * Translations for the current locale, already resolved: dictionary name →\n * key → string. This is the shape the server serializes onto the page, so it\n * is what the client reads at runtime — and it imports nothing.\n *\n * ```tsx\n * <Page translations={{ Chat: { greeting: \"Hello {{name}}\" } }}>\n * ```\n *\n * Merged over `dictionaries`, so the two compose: seed from the app's real\n * dictionary and override the one key a test is about.\n */\n translations?: Record<string, Record<string, string>>;\n /**\n * Data `useQuery` finds already cached, keyed by API path — the path passed\n * to `useQuery`, without the `/api` prefix the client adds when it fetches.\n * A key may carry a query string (`\"/lists?page=2\"`) to seed one search\n * variant.\n *\n * The cache is keyed by the *resolved* path, so a key's `:params` are\n * resolved against the page's `params` — and a key naming one the page does\n * not carry is an error, naming the key. That case is common enough to be\n * worth the loud failure: a query often takes its params from the call site\n * rather than the route (`useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state), and the right seed for it is the resolved\n * path, `\"/orgs/abc/lists\"`.\n *\n * Seeded at mount only: a component that mutates or refetches its data owns\n * the cache from then on.\n */\n queryData?: Record<string, unknown>;\n /** App-wide `useQuery` defaults, as `createRoot` threads them. */\n queryConfig?: QueryConfig;\n /**\n * What `useUser()` reports. Defaults to `null` — an anonymous visitor — and\n * either way the hook resolves from here without issuing a request.\n */\n user?: Partial<User> | null;\n /** What `useBreadcrumbs()` returns, in order. */\n breadcrumbs?: Breadcrumb[];\n /**\n * The theme `useTheme()` starts on. The app reads a visitor's stored choice\n * out of `localStorage`; a test has no session to have made one in, so this\n * seeds it without writing to storage. `setTheme` works from either.\n */\n theme?: \"light\" | \"dark\" | \"system\";\n /**\n * Fallback for the `Suspense` boundary wrapped around the children — the\n * stand-in for the view's own `Loading` export, which the real router\n * supplies. Defaults to `null`.\n */\n fallback?: ReactNode;\n /**\n * Renders in place of the children when one throws, mirroring a view's\n * `Error` export. Without it a thrown render (an errored suspense query,\n * say) propagates out of `render()` and fails the test, which is usually\n * what you want.\n */\n errorFallback?: ComponentType<FallbackProps>;\n /**\n * Called when something navigates — a `Link` click, `useNavigate().push`.\n * The navigation is reported, not performed: `<Page>` renders one route and\n * does not re-resolve the tree, so `useLocation()` keeps reporting the\n * pathname it was given.\n */\n onNavigate?: (href: string, action: \"push\" | \"replace\") => void;\n}\n\n/** `\"?a=b\"` (or `\"\"`) from any of the shapes `searchParams` accepts. */\nfunction toSearchString(search: PageProps[\"searchParams\"]): string {\n if (!search) return \"\";\n const query =\n typeof search === \"string\"\n ? search.replace(/^\\?/, \"\")\n : new URLSearchParams(\n Object.entries(search).map(([key, value]) => [key, String(value)]),\n ).toString();\n return query ? `?${query}` : \"\";\n}\n\n/**\n * The `:params` a seed key needs supplied. Wildcards (`:rest*`) are excluded —\n * `applyParams` drops those when missing, which is a resolution rather than a\n * failure.\n *\n * Optional params (`:id?`) cannot reach here at all: a key is split at its\n * first `?` to separate the search string, so `\"/lists/:folderId?\"` arrives as\n * the path `/lists/:folderId`. Seed the resolved path for those.\n */\nfunction requiredParamsOf(path: string): string[] {\n return Array.from(path.matchAll(/:([^/]+)/g), ([, name]) => name).filter(\n (name) => !name.endsWith(\"*\"),\n );\n}\n\n/**\n * Fails a `queryData` key whose `:params` the page does not carry, saying so\n * in terms of the key.\n *\n * Without this the two documented runners disagree, and neither is legible.\n * `applyParams` throws `Missing parameter: orgId` under `import.meta.env.DEV`\n * (vitest) — a router-utility stack trace out of `<Page>`'s render, before\n * anything mounts, naming nothing that appears in the test. Under `bun test`\n * that flag is falsy, so it logs and seeds the literal `/orgs/undefined/lists`\n * instead: the seed misses, the query goes to the network, and the assertion\n * runs against a loading state.\n *\n * The case is common because a query's params often come from the call site\n * rather than the route — `useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state. The cache is keyed by the *resolved* path,\n * so the way out is to seed that path directly.\n */\nfunction resolveSeedPath(\n key: string,\n rawPath: string,\n params: Record<string, string>,\n): string {\n const missing = requiredParamsOf(rawPath).filter(\n (name) => params[name] === undefined,\n );\n if (missing.length > 0) {\n throw new Error(\n `[gemi] <Page> cannot resolve the queryData key \"${key}\": no value for ` +\n `${missing.map((name) => `:${name}`).join(\", \")}. The query cache is ` +\n `keyed by the resolved path, so either add ${missing\n .map((name) => `\\`${name}\\``)\n .join(\", \")} to the page's \\`params\\`, or seed the resolved path ` +\n `(e.g. \"${applyParams(\n rawPath,\n Object.fromEntries(missing.map((name) => [name, \"123\"])),\n )}\") — which is what a query taking its params from component state ` +\n `reads under.`,\n );\n }\n return applyParams(rawPath, params);\n}\n\n/**\n * `queryData` in the shape the query cache seeds from: `{ [path]: { [variant]:\n * data } }`, the same payload `Query.prefetch` puts on a server-rendered page.\n */\nfunction toPrefetchedData(\n queryData: Record<string, unknown>,\n params: Record<string, string>,\n warned: Set<string>,\n) {\n const prefetchedData: Record<string, Record<string, unknown>> = {};\n for (const [key, data] of Object.entries(queryData)) {\n const [rawPath, rawSearch = \"\"] = key.split(\"?\");\n // Once per key per page, not once per render: this runs again on every\n // re-render, and a duplicated warning reads as a second mistake.\n if (rawPath.startsWith(\"/api/\") && !warned.has(key)) {\n warned.add(key);\n console.warn(\n `[gemi] queryData key \"${key}\" starts with /api. useQuery is keyed by ` +\n `the path you pass it — the /api prefix is added when it fetches — ` +\n `so this entry seeds a path no query reads.`,\n );\n }\n const path = resolveSeedPath(key, rawPath, params);\n prefetchedData[path] = {\n ...prefetchedData[path],\n [toVariantKey(rawSearch)]: data,\n };\n }\n return prefetchedData;\n}\n\n/**\n * `{ [locale]: { [dictionary]: { [key]: string } } }` — the payload\n * `Translator` serializes onto the page, which is the only form the client\n * ever sees. `dictionaries` are transposed into it (they are keyed the other\n * way round, by key then locale); `translations` is already in it and lands\n * under `locale`, last, so it wins.\n */\nfunction toClientDictionary(\n dictionaries: PageDictionary[],\n locales: string[],\n locale: string,\n translations: Record<string, Record<string, string>>,\n): Record<string, Record<string, Record<string, string>>> {\n const clientDictionary: Record<\n string,\n Record<string, Record<string, string>>\n > = {};\n for (const supported of locales) {\n clientDictionary[supported] ??= {};\n }\n for (const { name, dictionary } of dictionaries) {\n for (const [key, byLocale] of Object.entries(dictionary ?? {})) {\n for (const [dictLocale, translation] of Object.entries(byLocale ?? {})) {\n clientDictionary[dictLocale] ??= {};\n clientDictionary[dictLocale][name] ??= {};\n clientDictionary[dictLocale][name][key] = translation;\n }\n }\n }\n for (const [name, keys] of Object.entries(translations)) {\n clientDictionary[locale] ??= {};\n clientDictionary[locale][name] = {\n ...clientDictionary[locale][name],\n ...keys,\n };\n }\n return clientDictionary;\n}\n\n/**\n * Mounts a component with the inputs a view normally arrives with — route\n * params, the current locale and its dictionaries, prefetched query data, the\n * signed-in user — so a unit test can assert what it renders rather than only\n * its empty state.\n *\n * ```tsx\n * import { render, screen } from \"@testing-library/react\";\n * import { Page } from \"gemi/testing\";\n *\n * render(\n * <Page\n * pathname=\"/app/:orgId/chat\"\n * params={{ orgId: \"abc\" }}\n * queryData={{ \"/organizations/:orgId/messages\": [{ id: 1, body: \"hi\" }] }}\n * dictionaries={[ChatDictionary]}\n * >\n * <OrgChat />\n * </Page>,\n * );\n *\n * expect(screen.getByText(\"hi\")).toBeDefined();\n * ```\n *\n * It is a plain component, so it composes with any renderer — Testing\n * Library's `render`, `react-test-renderer`, or `renderToString`.\n *\n * Two things it does not do.\n *\n * It does not route: there is no view tree and no route manifest behind it, so\n * a navigation is reported through `onNavigate` rather than resolved. To\n * assert the page *after* one, render a second `<Page>`.\n *\n * And under `renderToString` it renders the server's own no-data branch for an\n * *unseeded* query rather than suspending — deliberately, because suspending\n * there is the streaming server's job: `createRoot` threads the request's\n * `ServerQueryStore` through `ServerQueryContext`, and that store is what runs\n * the route handler. A harness has no request and no handler to run, so there\n * is nothing to suspend on. Seeded queries render their data on the server\n * exactly as they do in the browser; unseeded ones behave as a route with no\n * `Query.prefetch` does, framework warning included.\n */\nexport const Page = (props: PropsWithChildren<PageProps>) => {\n const {\n children,\n pathname: routePath = \"/\",\n params = {},\n searchParams,\n hash = \"\",\n locale = \"en-US\",\n defaultLocale = locale,\n dictionaries = [],\n translations = {},\n queryData = {},\n queryConfig,\n user = null,\n breadcrumbs = [],\n theme,\n fallback = null,\n errorFallback,\n onNavigate,\n } = props;\n\n // The caller's list, in the caller's order — a language switcher maps over\n // it, so hoisting the page's own locale to the front would reorder the\n // rendered options. The defaults are appended, only to guarantee presence.\n const supportedLocales = useMemo(\n () =>\n Array.from(\n new Set([...(props.supportedLocales ?? []), defaultLocale, locale]),\n ),\n [props.supportedLocales, defaultLocale, locale],\n );\n const pathname = applyParams(routePath, params) || \"/\";\n const search = toSearchString(searchParams);\n // The URL's locale segment, which the default locale does not get.\n const urlLocaleSegment = locale === defaultLocale ? null : locale;\n\n const dictionary = useMemo(\n () =>\n toClientDictionary(dictionaries, supportedLocales, locale, translations),\n [dictionaries, supportedLocales, locale, translations],\n );\n\n // Keys that have already drawn a warning, so a re-render does not repeat it.\n const warnedRef = useRef<Set<string>>(new Set());\n const prefetchedData = useMemo(() => {\n const seeded = toPrefetchedData(queryData, params, warnedRef.current);\n // `useUser` reads `/auth/me` like any other query. Seeding it — with `null`\n // for the anonymous default — is what keeps a component test off the\n // network for a hook nothing in the test asked for.\n seeded[\"/auth/me\"] ??= { \"\": user ?? null };\n return seeded;\n }, [queryData, params, user]);\n\n // Keyed by `${view}:${pathname}` in the router's cache; the view names are\n // ours to choose here, and only their order reaches `useBreadcrumbs`.\n const { breadcrumbViews, breadcrumbsCache, breadcrumbsRecord } =\n useMemo(() => {\n const views = breadcrumbs.map((_, index) => `breadcrumb-${index}`);\n const cache = new Map<string, Breadcrumb>(\n breadcrumbs.map((breadcrumb, index) => [\n `${views[index]}:${pathname}`,\n breadcrumb,\n ]),\n );\n return {\n breadcrumbViews: views,\n breadcrumbsCache: cache,\n breadcrumbsRecord: Object.fromEntries(cache),\n };\n }, [breadcrumbs, pathname]);\n\n const [isNavigatingSubject] = useState(() => new Subject<boolean>(false));\n const [progressManager] = useState(\n () => new ProgressManager(isNavigatingSubject),\n );\n const [history] = useState(() =>\n createMemoryHistory({ initialEntries: [`${pathname}${search}${hash}`] }),\n );\n\n const routeState: RouteState & PageData = useMemo(\n () => ({\n views: [],\n params,\n search,\n state: {},\n pathname,\n hash,\n action: null,\n routePath,\n locale: urlLocaleSegment,\n data: {},\n i18n: { currentLocale: locale, dictionary, supportedLocales },\n prefetchedData,\n breadcrumbs: breadcrumbsRecord,\n appId: \"test\",\n }),\n [\n params,\n search,\n pathname,\n hash,\n routePath,\n urlLocaleSegment,\n locale,\n dictionary,\n supportedLocales,\n prefetchedData,\n breadcrumbsRecord,\n ],\n );\n\n const [routerSubject] = useState(() => new Subject<RouteState>(routeState));\n\n const onNavigateRef = useRef(onNavigate);\n onNavigateRef.current = onNavigate;\n // Subscribed during render, not in an effect, because React flushes child\n // effects before parent ones — and mount-time navigation is a real pattern:\n // `<Redirect>` pushes from its own mount effect, as does any auth guard. A\n // listener registered in `<Page>`'s effect is not there yet when those fire,\n // so `onNavigate` would silently miss exactly the redirects a test most\n // wants to assert. Ref-guarded, so React's double-invoke registers one.\n //\n // Nothing unsubscribes: this history is created per `<Page>` and reachable\n // from nowhere else, so it is collected with the page it belongs to.\n const listeningRef = useRef(false);\n if (!listeningRef.current) {\n listeningRef.current = true;\n history.listen(({ location, action }) => {\n const href = `${location.pathname}${location.search}${location.hash}`;\n onNavigateRef.current?.(\n href,\n action === Action.Replace ? \"replace\" : \"push\",\n );\n });\n }\n\n const serverData: ServerDataContextValue = useMemo(\n () => ({\n routeManifest: {},\n pageData: {},\n breadcrumbs: breadcrumbsRecord,\n prefetchedData,\n router: {\n pathname,\n params,\n currentPath: routePath,\n is404: false,\n searchParams: search,\n urlLocaleSegment,\n },\n i18n: {\n dictionary,\n currentLocale: locale,\n supportedLocales,\n defaultLocale,\n },\n componentTree: [],\n auth: { user: user as User },\n __csrf: \"test-csrf-token\",\n cssManifest: {},\n modulePreloadManifest: {},\n meta: {},\n appId: \"test\",\n }),\n [\n breadcrumbsRecord,\n prefetchedData,\n pathname,\n params,\n routePath,\n search,\n urlLocaleSegment,\n dictionary,\n locale,\n supportedLocales,\n defaultLocale,\n user,\n ],\n );\n\n // Everything the router hooks read, inert: nothing here fetches, preloads or\n // resolves a route, but every hook finds the shape it expects rather than an\n // empty context to crash on.\n const [viewEntriesSubject] = useState(() => new Subject<string[]>([]));\n const routerContext = useMemo(\n () => ({\n viewEntriesSubject,\n history,\n updatePageData: () => {},\n getPageData: () => ({}),\n getScrollPosition: () => 0,\n getViewPathsFromPathname: () => breadcrumbViews,\n getRoutePathnameFromHref: (href: string) => href,\n isNavigatingSubject,\n setNavigationAbortController: () => {},\n progressManager,\n fetchRouteCSS: async () => {},\n preloadRouteModules: () => {},\n prefetchRoute: async () => {},\n takePrefetched: () => null,\n clearPrefetchCache: () => {},\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n }),\n [\n viewEntriesSubject,\n history,\n breadcrumbViews,\n isNavigatingSubject,\n progressManager,\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n ],\n );\n\n const [websocket] = useState(() => ({\n subscribe: async () => {},\n unsubscribe: async () => {},\n broadcast: () => {},\n }));\n\n return (\n <ThemeProvider theme={theme}>\n <ServerDataContext.Provider value={serverData}>\n <I18nProvider>\n <WebSocketContext.Provider value={websocket}>\n <QueryManagerProvider queryConfig={queryConfig}>\n <ClientRouterContext.Provider value={routerContext}>\n <RouteTransitionProvider\n isPending={false}\n isFetching={false}\n transitionPath={[\"\", pathname]}\n >\n <RouteStateProvider state={routeState}>\n <Boundary\n errorFallback={errorFallback}\n fallback={fallback}\n resetKey={pathname}\n >\n {children}\n </Boundary>\n </RouteStateProvider>\n </RouteTransitionProvider>\n </ClientRouterContext.Provider>\n </QueryManagerProvider>\n </WebSocketContext.Provider>\n </I18nProvider>\n </ServerDataContext.Provider>\n </ThemeProvider>\n );\n};\n\n/**\n * The `Suspense` boundary every view renders inside, and — when the test\n * supplies a fallback for it — the error boundary too.\n *\n * `onReset={clearErrors}` is the part that has to be here rather than inline\n * above: it needs `QueryManagerContext`, and it is what makes a view's real\n * `Error` export testable. A fallback whose \"Try again\" calls\n * `resetErrorBoundary()` re-renders the child, and `useQuery` reads the\n * failure still stored on the `QueryResource` and throws it straight back —\n * so without this the fallback never goes away and the retry path that works\n * in the app cannot be exercised. `ClientRouter`'s own boundary wires exactly\n * this pair.\n */\nconst Boundary = (\n props: PropsWithChildren<{\n errorFallback?: ComponentType<FallbackProps>;\n fallback: ReactNode;\n resetKey: string;\n }>,\n) => {\n const { errorFallback: ErrorFallback, fallback, resetKey, children } = props;\n const { clearErrors } = useContext(QueryManagerContext);\n const content = <Suspense fallback={fallback}>{children}</Suspense>;\n\n if (!ErrorFallback) {\n return content;\n }\n\n return (\n <ErrorBoundary\n FallbackComponent={ErrorFallback}\n resetKeys={[resetKey]}\n onReset={clearErrors}\n >\n {content}\n </ErrorBoundary>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Subject.d.ts","sourceRoot":"","sources":["../../utils/Subject.ts"],"names":[],"mappings":"AAAA,qBAAa,OAAO,CAAC,CAAC;IACpB,WAAW,cAAmB,CAAC,KAAK,IAAI,EAAI;IAC5C,KAAK,EAAE,CAAC,CAAC;gBAEG,YAAY,EAAE,CAAC;IAIpB,SAAS,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI;IAOxC,IAAI,CAAC,KAAK,EAAE,CAAC;IAKb,QAAQ;CAGhB"}
1
+ {"version":3,"file":"Subject.d.ts","sourceRoot":"","sources":["../../utils/Subject.ts"],"names":[],"mappings":"AAAA,qBAAa,OAAO,CAAC,CAAC;IACpB,WAAW,cAAmB,CAAC,KAAK,IAAI,EAAI;IAC5C,KAAK,EAAE,CAAC,CAAC;gBAEG,YAAY,EAAE,CAAC;IAoBpB,SAAS,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI;IAOxC,IAAI,CAAC,KAAK,EAAE,CAAC;IAKb,QAAQ;CAGhB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The key one search-param combination is cached under, inside a query's
3
+ * resource: the params sorted and serialized, empty when there are none.
4
+ *
5
+ * Sorted because `?b=2&a=1` and `?a=1&b=2` are the same request, and a cache
6
+ * that keyed them separately would fetch twice and hydrate one of them from a
7
+ * payload the server produced for the other. Nullish values are dropped for
8
+ * the same reason: an optional filter left `undefined` is absent, not the
9
+ * string `"undefined"`.
10
+ *
11
+ * Every site that reads or writes the cache derives its key from here —
12
+ * `useQuery` (the read), `useMutate` (the write), and `gemi/testing`'s `<Page>`
13
+ * (the seed). They used to hold a copy each, which is the kind of duplication
14
+ * that fails quietly: a seed built by an old copy simply misses, and the test
15
+ * that should have read it fetches over the network and asserts an empty state
16
+ * with nothing pointing at the mismatch.
17
+ */
18
+ export declare function toVariantKey(search: string | Record<string, unknown> | null | undefined): string;
19
+ //# sourceMappingURL=variantKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variantKey.d.ts","sourceRoot":"","sources":["../../utils/variantKey.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAC1D,MAAM,CAQR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemi",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Enes Tufekci <enes@gemijs.dev>",
@@ -20,6 +20,7 @@
20
20
  "exports": {
21
21
  "./http": "./dist/http/index.js",
22
22
  "./client": "./dist/client/index.js",
23
+ "./testing": "./dist/testing/index.js",
23
24
  "./app": "./dist/app/index.js",
24
25
  "./facades": "./dist/facades/index.js",
25
26
  "./email": "./dist/email/index.js",
@@ -1,5 +0,0 @@
1
- // @bun
2
- import{Ae as O}from"./chunk-4t6a82fh.js";import{Re as M}from"./chunk-0h3jr9d9.js";var x={};M(x,{registeredNames:()=>S,register:()=>R,has:()=>_,get:()=>C,clearRegistry:()=>b});var D=new Map;function R(j,B){D.set(j,B)}function _(j){return D.has(j)}function C(j){let B=D.get(j);if(B===void 0)throw new O(j,[...D.keys()]);return B}function S(){return[...D.keys()]}function b(){D.clear()}import{AsyncLocalStorage as H}from"async_hooks";class K{content={title:"Gemi App",description:null,openGraph:null};touched=!1;render(){return{title:this.content.title,description:this.content.description,openGraph:this.content.openGraph}}title(j){this.touched=!0,this.content.title=j}description(j){this.touched=!0,this.content.description=j}openGraph({title:j,description:B,type:z,url:J,image:F,imageAlt:Q,imageWidth:V,imageHeight:X,twitterImage:Y=F,twitterImageAlt:N=Q,twitterImageWidth:P=V,twitterImageHeight:U=X}){this.touched=!0;let Z=F,$=Y;if(F&&!F.startsWith("http"))Z=`${process.env.HOST_NAME}${F}`,$=`${process.env.HOST_NAME}${Y}`;this.content.openGraph=Object.fromEntries(Object.entries({title:j,description:B,type:z,url:J,image:Z,imageAlt:Q,imageWidth:V,imageHeight:X,twitterImage:$,twitterImageAlt:N,twitterImageWidth:P,twitterImageHeight:U}).filter(([A,T])=>T!==void 0))}}function E(j,B,z={}){return[`${j}=${B}`,z.maxAge?`Max-Age=${z.maxAge}`:"",z.httpOnly?"HttpOnly":"",z.secure?"Secure":"",z.sameSite?`SameSite=${z.sameSite}`:"SameSite=Strict",z.path?`Path=${z.path}`:"Path=/",z.domain?`Domain=${z.domain}`:"",z.expires?`Expires=${z.expires.toUTCString()}`:"",z.partitioned?"Partitioned":""].filter((J)=>J!=="").join("; ")}var G=new H;class L{req;cookies=new Set;headers=new Headers;serverQueries=null;user=null;csrfHmac=null;locale=null;metadata=new K;rangeRequest=null;constructor(j){this.req=j}setLocale(j){this.locale=j}renderMeta(){return this.metadata.render()}setCSRFHmac(j){this.csrfHmac=j}setCookie(j,B,z={}){this.cookies.add(E(j,B,z))}deleteCookie(j){this.cookies.add(E(j,"",{maxAge:-1}))}setHeaders(j,B){this.headers.set(j,B)}setUser(j){this.user=j}setRequest(j){this.req=j}destroy(){delete this.cookies,delete this.headers,this.serverQueries=null,delete this.user}}class W{static getStore(){return G.getStore()}static setRequest(j){G.getStore().req=j}static run(j,B){return G.run(new L(j),B)}static runWith(j,B){return G.run(j,B)}}
3
- export{W as ae,R as be,_ as ce,C as de,S as ee,x as fe};
4
-
5
- //# debugId=7F460FF2666379D464756E2164756E21