@gusnips/react 0.6.0 → 0.6.1

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.
@@ -146,6 +146,20 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
146
146
  * somebody. A name that is absent falls back to the server's English `message` instead.
147
147
  */
148
148
  knownMessageKeys: Record<ServerName, unknown>;
149
+ /**
150
+ * Transform `params` before they fill the server's sentence.
151
+ *
152
+ * The envelope carries what the SERVER knows, which is not always a word anyone should read: a
153
+ * role id (`admin`), an action verb (`approve_content`). The sentence needs whatever the app's
154
+ * own screens call those things, and the substitution happens here — inside the one lookup that
155
+ * reads `params` — so there is nowhere else an adopter could intervene. Without this seam the
156
+ * choice is to ship the raw id into `{{role}}`, or to stop using the server's sentence at all
157
+ * and re-resolve every key by hand, which is how i18n ended up inside a transport once already.
158
+ *
159
+ * Optional, and identity by default: an API whose params are already words passes nothing. It
160
+ * deliberately takes only the params — an arm that needs the error has `ctx` for that.
161
+ */
162
+ localizeParams?: (params: Record<string, string | number> | undefined) => Record<string, unknown> | undefined;
149
163
  /**
150
164
  * Per-code copy. Everything not listed falls through to the default arm.
151
165
  *
@@ -167,6 +181,6 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
167
181
  }
168
182
  export type ErrorArm = (ctx: ErrorContext) => ErrorCopy;
169
183
  /** Build the describer. */
170
- export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, codes, durableLimitCodes, maxRetryWaitSecs, }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
184
+ export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, localizeParams, codes, durableLimitCodes, maxRetryWaitSecs, }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
171
185
  export {};
172
186
  //# sourceMappingURL=describe-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@ export function humanizeWait(t, secs, prefix) {
21
21
  return t(`${prefix}waitHours`, { count: Math.round(secs / 3600) });
22
22
  }
23
23
  /** Build the describer. */
24
- export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, codes = {}, durableLimitCodes = [], maxRetryWaitSecs, }) {
24
+ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, localizeParams = (params) => params, codes = {}, durableLimitCodes = [], maxRetryWaitSecs, }) {
25
25
  const known = new Set(Object.keys(knownMessageKeys));
26
26
  // Sound, and the reason nothing here needs a cast: the set was built from the keys of
27
27
  // `knownMessageKeys`, so membership really does establish the claim. `Object.keys` and not
@@ -32,7 +32,7 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
32
32
  if (named === undefined || !named.startsWith(messageKeyPrefix))
33
33
  return null;
34
34
  const name = named.slice(messageKeyPrefix.length);
35
- return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
35
+ return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
36
36
  };
37
37
  // `messageKey` first: an API that sends both means the key, and the code is what the UI
38
38
  // switches on. The code is only consulted when there is no key to prefer, and it is spelled
@@ -1 +1 @@
1
- {"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -220,6 +220,44 @@ describe("the recovery kind", () => {
220
220
  });
221
221
  });
222
222
 
223
+ /**
224
+ * The seventh migration's seam. The server names what IT knows — a role id, an action verb — and
225
+ * the sentence needs whatever the app's own screens call those. The substitution can only happen
226
+ * inside the lookup that reads `params`, which is why an arm cannot do it: by the time an arm
227
+ * runs, `ctx.says` is already resolved.
228
+ */
229
+ describe("localizeParams", () => {
230
+ const options = {
231
+ t,
232
+ copyPrefix: "errors." as const,
233
+ formatWait: (secs: number) => humanizeWait(t, secs, "errors."),
234
+ messageKeyPrefix: "serverErrors." as const,
235
+ knownMessageKeys: { roleRequired: "" },
236
+ };
237
+ const refusal = new ApiError(403, {
238
+ code: "FORBIDDEN",
239
+ message: "Needs admin",
240
+ messageKey: "serverErrors.roleRequired",
241
+ params: { role: "admin" },
242
+ });
243
+
244
+ it("translates a param before it fills the sentence", () => {
245
+ const describeError = createErrorDescriber({
246
+ ...options,
247
+ localizeParams: (params) => ({
248
+ ...params,
249
+ ...(typeof params?.role === "string" && { role: t(`roles.${params.role}`) }),
250
+ }),
251
+ });
252
+ expect(describeError(refusal).cause).toBe('serverErrors.roleRequired({"role":"roles.admin"})');
253
+ });
254
+
255
+ it("is identity by default, so params already in words pass straight through", () => {
256
+ const describeError = createErrorDescriber(options);
257
+ expect(describeError(refusal).cause).toBe('serverErrors.roleRequired({"role":"admin"})');
258
+ });
259
+ });
260
+
223
261
  /**
224
262
  * The second convention in the fleet: an API with no `messageKey` field at all, whose CODE
225
263
  * names the sentence. One repo has 98 of them — `errors.FORECAST_NOT_FOUND` and its siblings —
@@ -197,6 +197,22 @@ export interface ErrorDescriberOptions<
197
197
  * somebody. A name that is absent falls back to the server's English `message` instead.
198
198
  */
199
199
  knownMessageKeys: Record<ServerName, unknown>;
200
+ /**
201
+ * Transform `params` before they fill the server's sentence.
202
+ *
203
+ * The envelope carries what the SERVER knows, which is not always a word anyone should read: a
204
+ * role id (`admin`), an action verb (`approve_content`). The sentence needs whatever the app's
205
+ * own screens call those things, and the substitution happens here — inside the one lookup that
206
+ * reads `params` — so there is nowhere else an adopter could intervene. Without this seam the
207
+ * choice is to ship the raw id into `{{role}}`, or to stop using the server's sentence at all
208
+ * and re-resolve every key by hand, which is how i18n ended up inside a transport once already.
209
+ *
210
+ * Optional, and identity by default: an API whose params are already words passes nothing. It
211
+ * deliberately takes only the params — an arm that needs the error has `ctx` for that.
212
+ */
213
+ localizeParams?: (
214
+ params: Record<string, string | number> | undefined,
215
+ ) => Record<string, unknown> | undefined;
200
216
  /**
201
217
  * Per-code copy. Everything not listed falls through to the default arm.
202
218
  *
@@ -230,6 +246,7 @@ export function createErrorDescriber<
230
246
  formatWait,
231
247
  messageKeyPrefix,
232
248
  knownMessageKeys,
249
+ localizeParams = (params) => params,
233
250
  codes = {},
234
251
  durableLimitCodes = [],
235
252
  maxRetryWaitSecs,
@@ -245,7 +262,7 @@ export function createErrorDescriber<
245
262
  const lookup = (named: string | undefined): string | null => {
246
263
  if (named === undefined || !named.startsWith(messageKeyPrefix)) return null;
247
264
  const name = named.slice(messageKeyPrefix.length);
248
- return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
265
+ return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
249
266
  };
250
267
  // `messageKey` first: an API that sends both means the key, and the code is what the UI
251
268
  // switches on. The code is only consulted when there is no key to prefer, and it is spelled
@@ -0,0 +1,129 @@
1
+ import type { ReactNode } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ createRequireAnonymous,
5
+ createRequireAuth,
6
+ createRequireProfile,
7
+ type MeQuery,
8
+ } from "./guards.tsx";
9
+
10
+ // `react-router-dom` is mocked rather than wrapped in a real router. Every question here is only
11
+ // WHICH branch a guard picked, and a redirect is the one branch with no children to read — so
12
+ // `Navigate` never has to run, it only has to be identifiable as an element type. That is what
13
+ // lets the redirect cases assert a destination without rendering anything.
14
+ const router = vi.hoisted(() => ({
15
+ Navigate: vi.fn(),
16
+ useLocation: vi.fn(() => ({ pathname: "/posts", search: "?page=2" })),
17
+ }));
18
+ vi.mock("react-router-dom", () => router);
19
+
20
+ /**
21
+ * One render, as plain data.
22
+ *
23
+ * A function component IS a function, so calling it returns the element it would have rendered —
24
+ * no DOM, no renderer, nothing mounted, and no test-only dependency to install. The cast is this
25
+ * file admitting it reads two fields off that element and nothing else.
26
+ */
27
+ type Rendered = { type: unknown; props: Record<string, unknown> };
28
+ const render = (Guard: (p: { children: ReactNode }) => ReactNode): Rendered =>
29
+ Guard({ children: "CONTENT" }) as unknown as Rendered;
30
+
31
+ /** Which branch came back: a redirect names its destination, every other branch names its child. */
32
+ const branch = (r: Rendered): unknown =>
33
+ r.type === router.Navigate ? { to: r.props.to, state: r.props.state } : r.props.children;
34
+
35
+ const session =
36
+ (isAuthenticated: boolean, isLoading = false) =>
37
+ () => ({
38
+ isAuthenticated,
39
+ isLoading,
40
+ });
41
+
42
+ const profile = (me: MeQuery<{ suspended: boolean }>) =>
43
+ createRequireProfile(() => me, { loading: "LOADING" })(
44
+ (m) => !m.suspended,
45
+ "DENIED",
46
+ (error) => `FAILED:${String(error)}`,
47
+ );
48
+
49
+ describe("createRequireProfile", () => {
50
+ it("waits while the profile is still being asked for", () => {
51
+ expect(branch(render(profile({ status: "pending" })))).toBe("LOADING");
52
+ });
53
+
54
+ // Invariant 4, and the reason this module exists rather than being written a ninth time.
55
+ // `!me?.isStaff` reads an undefined profile as a real "no", so an outage on /auth/me becomes a
56
+ // refusal — the wrong cause, no retry, and no request id to quote. Three repos shipped that, and
57
+ // in two of them the sibling guard failed the other way for the same reason: `me?.suspended` read
58
+ // a failure as "not suspended" and let a suspended account straight through. One mistake, closed
59
+ // on one screen and OPEN on the next, which is why a failure must reach neither answer.
60
+ it("does not read a failed query as a refusal", () => {
61
+ const failed = branch(render(profile({ status: "error", error: "boom" })));
62
+ expect(failed).toBe("FAILED:boom");
63
+ expect(failed).not.toBe("DENIED");
64
+ expect(failed).not.toBe("CONTENT");
65
+ });
66
+
67
+ it("refuses only on a real no", () => {
68
+ expect(branch(render(profile({ status: "success", data: { suspended: true } })))).toBe(
69
+ "DENIED",
70
+ );
71
+ });
72
+
73
+ it("lets an allowed profile through", () => {
74
+ expect(branch(render(profile({ status: "success", data: { suspended: false } })))).toBe(
75
+ "CONTENT",
76
+ );
77
+ });
78
+
79
+ it("takes a refusal as a function, for one built only when it is used", () => {
80
+ const Guard = createRequireProfile(() => ({ status: "success" as const, data: 1 }), {
81
+ loading: "LOADING",
82
+ })(
83
+ () => false,
84
+ () => "DENIED-LAZY",
85
+ () => "FAILED",
86
+ );
87
+ expect(branch(render(Guard))).toBe("DENIED-LAZY");
88
+ });
89
+ });
90
+
91
+ describe("createRequireAuth", () => {
92
+ const guard = (isAuthenticated: boolean, isLoading = false) =>
93
+ createRequireAuth(session(isAuthenticated, isLoading), "/login", { loading: "LOADING" });
94
+
95
+ it("waits while the session is still resolving", () => {
96
+ expect(branch(render(guard(false, true)))).toBe("LOADING");
97
+ });
98
+
99
+ // The redirect carries where they were headed, query string included: an adopter's sign-in screen
100
+ // reads `location.state.from` to land them on the page they actually wanted rather than the home
101
+ // screen. One donor hardcoded this in four places, which is what made it a factory.
102
+ it("remembers the address it turned away", () => {
103
+ expect(branch(render(guard(false)))).toEqual({
104
+ to: "/login",
105
+ state: { from: "/posts?page=2" },
106
+ });
107
+ });
108
+
109
+ it("lets a signed-in visitor through", () => {
110
+ expect(branch(render(guard(true)))).toBe("CONTENT");
111
+ });
112
+ });
113
+
114
+ describe("createRequireAnonymous", () => {
115
+ const guard = (isAuthenticated: boolean, isLoading = false) =>
116
+ createRequireAnonymous(session(isAuthenticated, isLoading), "/", { loading: "LOADING" });
117
+
118
+ it("waits while the session is still resolving", () => {
119
+ expect(branch(render(guard(true, true)))).toBe("LOADING");
120
+ });
121
+
122
+ it("sends a signed-in visitor home", () => {
123
+ expect(branch(render(guard(true)))).toEqual({ to: "/", state: undefined });
124
+ });
125
+
126
+ it("lets an anonymous visitor through", () => {
127
+ expect(branch(render(guard(false)))).toBe("CONTENT");
128
+ });
129
+ });