@gusnips/react 0.3.0 → 0.4.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 CHANGED
@@ -128,14 +128,22 @@ hurry cannot skip.
128
128
 
129
129
  ## Also here
130
130
 
131
- `hydrateOrMount` for prerendered pages, `ErrorBoundary`, an SSE reader split into a platform-free
132
- parser and a stream wrapper, `i18nInitOptions`, and `cn`.
131
+ `ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
132
+ `i18nInitOptions`, and `cn`.
133
133
 
134
134
  `createAuthStore` is at `@gusnips/react/store` — a zustand store whose `isLoading` starts `false`
135
135
  where there is no window. A session bootstrap can only be in flight in a browser, and `true`
136
136
  during a build is a wait that never ends: it once shipped a spinner as the indexable body of a
137
137
  page whose whole job was to be found.
138
138
 
139
+ `hydrateOrMount` is at `@gusnips/react/hydrate`, and the guards are at `@gusnips/react/guards`.
140
+
141
+ ## On a phone
142
+
143
+ The main entry works in React Native. It imports `react` and nothing else you would have to go
144
+ find — no `react-dom`, no router, no store — so the fetch client, the retry rule and the SSE
145
+ parser all come along, and the four things that need a browser stay behind the subpaths above.
146
+
139
147
  ## The Base UI wrappers
140
148
 
141
149
  ```ts
@@ -64,6 +64,12 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
64
64
  /**
65
65
  * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
66
66
  * ignored — the server names a sentence, it does not get to name any key in the app.
67
+ *
68
+ * The `code` is read under this prefix too, for an API that has no `messageKey` field and
69
+ * lets the code name the sentence — one repo in the fleet has 98 of those, `errors.NOT_FOUND`
70
+ * and its siblings, against another repo's `messageKey`. Both are the same claim ("the server
71
+ * named a sentence this app carries"), both pass the same gate below, and an app that uses
72
+ * neither is unaffected because nothing matches.
67
73
  */
68
74
  messageKeyPrefix: ServerPrefix;
69
75
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,IAAI,CACnD,GAAG,EAAE,GAAG,EACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ,wFAAwF;AACxF,KAAK,QAAQ,GAAG,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;AAE5D,iGAAiG;AACjG,KAAK,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;AA8BlF,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,EAChD,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAIR;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB,CACpC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM;IAEzB,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,GAAG,GAAG,YAAY,GAAG,UAAU,EAAE,CAAC,CAAC;IACtE,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,gBAAgB,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC9C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,cAAc,CAAC;AAE7D,2BAA2B;AAC3B,wBAAgB,oBAAoB,CAClC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM,EACzB,EACA,CAAC,EACD,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,KAAU,GACX,EAAE,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CA0C9F"}
1
+ {"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,IAAI,CACnD,GAAG,EAAE,GAAG,EACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ,wFAAwF;AACxF,KAAK,QAAQ,GAAG,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;AAE5D,iGAAiG;AACjG,KAAK,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;AA8BlF,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,EAChD,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAIR;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB,CACpC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM;IAEzB,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,GAAG,GAAG,YAAY,GAAG,UAAU,EAAE,CAAC,CAAC;IACtE,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,gBAAgB,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC9C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,cAAc,CAAC;AAE7D,2BAA2B;AAC3B,wBAAgB,oBAAoB,CAClC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM,EACzB,EACA,CAAC,EACD,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,KAAU,GACX,EAAE,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAkD9F"}
@@ -30,11 +30,17 @@ export function createErrorDescriber({ t, copyPrefix, messageKeyPrefix, knownMes
30
30
  // `in`, which would have accepted "toString".
31
31
  const has = (name) => known.has(name);
32
32
  function serverSentence(error) {
33
- const messageKey = error.messageKey;
34
- if (messageKey === undefined || !messageKey.startsWith(messageKeyPrefix))
35
- return null;
36
- const name = messageKey.slice(messageKeyPrefix.length);
37
- return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
33
+ const lookup = (named) => {
34
+ if (named === undefined || !named.startsWith(messageKeyPrefix))
35
+ return null;
36
+ const name = named.slice(messageKeyPrefix.length);
37
+ return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
38
+ };
39
+ // `messageKey` first: an API that sends both means the key, and the code is what the UI
40
+ // switches on. The code is only consulted when there is no key to prefer, and it is spelled
41
+ // under the same prefix so the gate is identical.
42
+ return (lookup(error.messageKey) ??
43
+ (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`)));
38
44
  }
39
45
  return function describeError(error) {
40
46
  if (!(error instanceof ApiError)) {
@@ -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/dist/hydrate.d.ts CHANGED
@@ -8,6 +8,11 @@
8
8
  *
9
9
  * The constants themselves live in `prerender-contract.ts`, which imports nothing, because the
10
10
  * BUILD needs them too and must not pull `react-dom/client` into a Node process to get a string.
11
+ *
12
+ * This module sits behind `@gusnips/react/hydrate` rather than in the barrel for the same reason
13
+ * one rung further out: it is the package's ONLY `react-dom` import, and a React Native app has
14
+ * no react-dom to give. Leaving it in the barrel made `react-dom` a required peer and shut the
15
+ * whole package out of every phone — for one function a phone would never call.
11
16
  */
12
17
  import type { ReactNode } from "react";
13
18
  export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGpF"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGpF"}
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAa;IAC5E,IAAI,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,KAAK,KAAK;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAChF,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAa;IAC5E,IAAI,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,KAAK,KAAK;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAChF,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { queryDefaults, shouldRetry, type QueryDefaultsOptions } from "./query.t
6
6
  export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
7
7
  export { createErrorDescriber, humanizeWait, retryAfterSecs, type DescribedError, type ErrorArm, type ErrorContext, type ErrorDescriberOptions, type Translate, } from "./describe-error.ts";
8
8
  export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError, } from "./chunk-reload.ts";
9
- export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.ts";
9
+ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
10
10
  export { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
11
11
  export type { EmptyStateProps, ErrorStateProps } from "./states.ts";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -11,13 +11,21 @@
11
11
  // @gusnips/react/ui the Base UI wrappers → @base-ui/react
12
12
  // @gusnips/react/store createAuthStore → zustand
13
13
  // @gusnips/react/guards the route guards → react-router-dom
14
+ // @gusnips/react/hydrate hydrateOrMount → react-dom
14
15
  //
15
- // So what is left here imports `react`, `react-dom` and `@gusnips/http` and nothing else, and an
16
- // app on TanStack Router or on Redux takes the fetch client without installing a router or a
17
- // store it will never call. `@tanstack/react-query`, `i18next` and `react-i18next` stay optional
18
- // AND stay here, because `query.ts`, `i18n.ts` and `states.ts` import only their TYPES — which
19
- // erase, so the built barrel does not reference them at runtime. Check that claim against
20
- // `dist/`, never against this file: a type-only re-export looks identical in source.
16
+ // `react-dom` is the one the second migration added, and it is the sharpest case yet: olhary
17
+ // ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to give.
18
+ // A required peer nobody can satisfy is not a strict contract, it is a closed door — the whole
19
+ // package was unusable there, for one function no phone would ever call. The rule did not need
20
+ // changing to catch it; `react-dom` simply had to stop being required.
21
+ //
22
+ // So what is left here imports `react` and `@gusnips/http` and nothing else, and an app on
23
+ // TanStack Router, on Redux, or on a phone takes the fetch client without installing a router, a
24
+ // store or a DOM renderer it will never call. `@tanstack/react-query`, `i18next` and
25
+ // `react-i18next` stay optional AND stay here, because `query.ts`, `i18n.ts` and `states.ts`
26
+ // import only their TYPES — which erase, so the built barrel does not reference them at runtime.
27
+ // Check that claim against `dist/`, never against this file: a type-only re-export looks
28
+ // identical in source.
21
29
  export { cn } from "./cn.js";
22
30
  export { ApiError, isAbortError } from "./api-error.js";
23
31
  export { createApiClient, } from "./api-client.js";
@@ -26,6 +34,9 @@ export { queryDefaults, shouldRetry } from "./query.js";
26
34
  export { ErrorBoundary, } from "./error-boundary.js";
27
35
  export { createErrorDescriber, humanizeWait, retryAfterSecs, } from "./describe-error.js";
28
36
  export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError, } from "./chunk-reload.js";
29
- export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.js";
37
+ // The two constants, but NOT `hydrateOrMount` — that one is at `@gusnips/react/hydrate`, because
38
+ // it is the package's only `react-dom` import. These come from the contract module, which imports
39
+ // nothing at all, so they are free to everyone: a build script, a browser entry, or a phone.
40
+ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.js";
30
41
  export { applyBrandVars, i18nInitOptions } from "./i18n.js";
31
42
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,iGAAiG;AACjG,+FAA+F;AAC/F,0FAA0F;AAC1F,qFAAqF;AAErF,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,GAMf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,+DAA+D;AAC/D,EAAE;AACF,6FAA6F;AAC7F,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,uEAAuE;AACvE,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,GAMf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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, chunk-reload recovery, i18n bootstrap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,6 +26,10 @@
26
26
  "types": "./dist/guards.d.ts",
27
27
  "default": "./dist/guards.js"
28
28
  },
29
+ "./hydrate": {
30
+ "types": "./dist/hydrate.d.ts",
31
+ "default": "./dist/hydrate.js"
32
+ },
29
33
  "./contract": {
30
34
  "types": "./dist/prerender-contract.d.ts",
31
35
  "default": "./dist/prerender-contract.js"
@@ -92,6 +96,9 @@
92
96
  "i18next": {
93
97
  "optional": true
94
98
  },
99
+ "react-dom": {
100
+ "optional": true
101
+ },
95
102
  "react-i18next": {
96
103
  "optional": true
97
104
  },
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isChunkLoadError, isPreloadHintFailure } from "./chunk-reload.ts";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError } from "./chunk-reload.ts";
3
3
 
4
4
  // No browser gives a failed dynamic import a shared type, so this is matched by message and
5
5
  // every phrasing has to be here. A miss is a white screen after a deploy — the exact failure
@@ -30,6 +30,9 @@ describe("isChunkLoadError", () => {
30
30
  it("does not match an ordinary render error", () => {
31
31
  expect(isChunkLoadError(new TypeError("Cannot read properties of undefined"))).toBe(false);
32
32
  expect(isChunkLoadError(null)).toBe(false);
33
+ // A thrown string reaches the `String(error)` branch, which is the one a non-Error throw
34
+ // takes — and the one that would blow up if it were written `error.message`.
35
+ expect(isChunkLoadError("something failed")).toBe(false);
33
36
  });
34
37
  });
35
38
 
@@ -44,3 +47,67 @@ describe("isPreloadHintFailure", () => {
44
47
  );
45
48
  });
46
49
  });
50
+
51
+ /**
52
+ * The guard, which is the whole reason this function exists rather than a bare
53
+ * `location.reload()` at the call site. Reloading on a chunk error the reload does not fix is
54
+ * an infinite loop with a person inside it, and it only reproduces on a real broken deploy —
55
+ * so this is the only place the rule gets checked.
56
+ *
57
+ * Two globals, stubbed rather than run under a DOM: `window` and `sessionStorage` are the only
58
+ * ones this module touches, and adding jsdom to a package whose whole point is a small
59
+ * dependency list would cost more than it explains.
60
+ */
61
+ describe("reloadOnceForChunkError", () => {
62
+ const reload = vi.fn();
63
+ let store: Record<string, string> = {};
64
+ /** Throws from both methods, the way a browser does when site data is blocked. */
65
+ let blocked = false;
66
+
67
+ beforeEach(() => {
68
+ vi.useFakeTimers();
69
+ reload.mockClear();
70
+ store = {};
71
+ blocked = false;
72
+ vi.stubGlobal("window", { location: { reload } });
73
+ vi.stubGlobal("sessionStorage", {
74
+ getItem: (key: string): string | null => {
75
+ if (blocked) throw new Error("site data blocked");
76
+ return store[key] ?? null;
77
+ },
78
+ setItem: (key: string, value: string): void => {
79
+ if (blocked) throw new Error("site data blocked");
80
+ store[key] = value;
81
+ },
82
+ });
83
+ });
84
+
85
+ afterEach(() => {
86
+ vi.unstubAllGlobals();
87
+ vi.useRealTimers();
88
+ });
89
+
90
+ it("reloads on the first chunk failure", () => {
91
+ expect(reloadOnceForChunkError()).toBe(true);
92
+ expect(reload).toHaveBeenCalledTimes(1);
93
+ });
94
+
95
+ it("refuses a second reload inside the window, so a dead chunk cannot loop", () => {
96
+ expect(reloadOnceForChunkError()).toBe(true);
97
+ expect(reloadOnceForChunkError()).toBe(false);
98
+ expect(reload).toHaveBeenCalledTimes(1);
99
+ });
100
+
101
+ it("allows another one after the window, because the next deploy is a new problem", () => {
102
+ expect(reloadOnceForChunkError()).toBe(true);
103
+ vi.setSystemTime(Date.now() + 61_000);
104
+ expect(reloadOnceForChunkError()).toBe(true);
105
+ expect(reload).toHaveBeenCalledTimes(2);
106
+ });
107
+
108
+ it("still reloads when storage is blocked — a privacy setting is not a reason to strand", () => {
109
+ blocked = true;
110
+ expect(reloadOnceForChunkError()).toBe(true);
111
+ expect(reload).toHaveBeenCalledTimes(1);
112
+ });
113
+ });
@@ -102,3 +102,39 @@ describe("createErrorDescriber", () => {
102
102
  expect(describeError(new ApiError(403, null)).hint).toBeUndefined();
103
103
  });
104
104
  });
105
+
106
+ /**
107
+ * The second convention in the fleet: an API with no `messageKey` field at all, whose CODE
108
+ * names the sentence. One repo has 98 of them — `errors.FORECAST_NOT_FOUND` and its siblings —
109
+ * and its client used to resolve them itself, which is how the translation ended up inside the
110
+ * transport.
111
+ */
112
+ describe("createErrorDescriber, when the code names the sentence", () => {
113
+ const describeError = createErrorDescriber({
114
+ t,
115
+ copyPrefix: "errors.",
116
+ messageKeyPrefix: "errors.",
117
+ knownMessageKeys: { FORECAST_NOT_FOUND: "", network: "" },
118
+ });
119
+
120
+ it("resolves a code this build has copy for", () => {
121
+ const error = new ApiError(404, { code: "FORECAST_NOT_FOUND", message: "Forecast not found" });
122
+ expect(describeError(error).cause).toBe("errors.FORECAST_NOT_FOUND");
123
+ });
124
+
125
+ it("falls back to the server's own message for a code with no copy", () => {
126
+ // The server writes for a developer, so this is the worse of the two — but it is still the
127
+ // most specific thing anyone has, and support can act on it.
128
+ const error = new ApiError(429, { code: "RESEARCH_LIMIT", message: "Concurrent limit hit" });
129
+ expect(describeError(error).cause).toBe("Concurrent limit hit");
130
+ });
131
+
132
+ it("prefers messageKey when the server sends both", () => {
133
+ const error = new ApiError(404, {
134
+ code: "FORECAST_NOT_FOUND",
135
+ message: "Forecast not found",
136
+ messageKey: "errors.network",
137
+ });
138
+ expect(describeError(error).cause).toBe("errors.network");
139
+ });
140
+ });
@@ -121,6 +121,12 @@ export interface ErrorDescriberOptions<
121
121
  /**
122
122
  * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
123
123
  * ignored — the server names a sentence, it does not get to name any key in the app.
124
+ *
125
+ * The `code` is read under this prefix too, for an API that has no `messageKey` field and
126
+ * lets the code name the sentence — one repo in the fleet has 98 of those, `errors.NOT_FOUND`
127
+ * and its siblings, against another repo's `messageKey`. Both are the same claim ("the server
128
+ * named a sentence this app carries"), both pass the same gate below, and an app that uses
129
+ * neither is unaffected because nothing matches.
124
130
  */
125
131
  messageKeyPrefix: ServerPrefix;
126
132
  /**
@@ -168,10 +174,18 @@ export function createErrorDescriber<
168
174
  const has = (name: string): name is ServerName => known.has(name);
169
175
 
170
176
  function serverSentence(error: ApiError): string | null {
171
- const messageKey = error.messageKey;
172
- if (messageKey === undefined || !messageKey.startsWith(messageKeyPrefix)) return null;
173
- const name = messageKey.slice(messageKeyPrefix.length);
174
- return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
177
+ const lookup = (named: string | undefined): string | null => {
178
+ if (named === undefined || !named.startsWith(messageKeyPrefix)) return null;
179
+ const name = named.slice(messageKeyPrefix.length);
180
+ return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
181
+ };
182
+ // `messageKey` first: an API that sends both means the key, and the code is what the UI
183
+ // switches on. The code is only consulted when there is no key to prefer, and it is spelled
184
+ // under the same prefix so the gate is identical.
185
+ return (
186
+ lookup(error.messageKey) ??
187
+ (error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`))
188
+ );
175
189
  }
176
190
 
177
191
  return function describeError(error: unknown): DescribedError {
package/src/hydrate.ts CHANGED
@@ -8,6 +8,11 @@
8
8
  *
9
9
  * The constants themselves live in `prerender-contract.ts`, which imports nothing, because the
10
10
  * BUILD needs them too and must not pull `react-dom/client` into a Node process to get a string.
11
+ *
12
+ * This module sits behind `@gusnips/react/hydrate` rather than in the barrel for the same reason
13
+ * one rung further out: it is the package's ONLY `react-dom` import, and a React Native app has
14
+ * no react-dom to give. Leaving it in the barrel made `react-dom` a required peer and shut the
15
+ * whole package out of every phone — for one function a phone would never call.
11
16
  */
12
17
  import type { ReactNode } from "react";
13
18
  import { createRoot, hydrateRoot } from "react-dom/client";
package/src/index.ts CHANGED
@@ -11,13 +11,21 @@
11
11
  // @gusnips/react/ui the Base UI wrappers → @base-ui/react
12
12
  // @gusnips/react/store createAuthStore → zustand
13
13
  // @gusnips/react/guards the route guards → react-router-dom
14
+ // @gusnips/react/hydrate hydrateOrMount → react-dom
14
15
  //
15
- // So what is left here imports `react`, `react-dom` and `@gusnips/http` and nothing else, and an
16
- // app on TanStack Router or on Redux takes the fetch client without installing a router or a
17
- // store it will never call. `@tanstack/react-query`, `i18next` and `react-i18next` stay optional
18
- // AND stay here, because `query.ts`, `i18n.ts` and `states.ts` import only their TYPES — which
19
- // erase, so the built barrel does not reference them at runtime. Check that claim against
20
- // `dist/`, never against this file: a type-only re-export looks identical in source.
16
+ // `react-dom` is the one the second migration added, and it is the sharpest case yet: olhary
17
+ // ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to give.
18
+ // A required peer nobody can satisfy is not a strict contract, it is a closed door — the whole
19
+ // package was unusable there, for one function no phone would ever call. The rule did not need
20
+ // changing to catch it; `react-dom` simply had to stop being required.
21
+ //
22
+ // So what is left here imports `react` and `@gusnips/http` and nothing else, and an app on
23
+ // TanStack Router, on Redux, or on a phone takes the fetch client without installing a router, a
24
+ // store or a DOM renderer it will never call. `@tanstack/react-query`, `i18next` and
25
+ // `react-i18next` stay optional AND stay here, because `query.ts`, `i18n.ts` and `states.ts`
26
+ // import only their TYPES — which erase, so the built barrel does not reference them at runtime.
27
+ // Check that claim against `dist/`, never against this file: a type-only re-export looks
28
+ // identical in source.
21
29
 
22
30
  export { cn } from "./cn.ts";
23
31
 
@@ -57,7 +65,10 @@ export {
57
65
  reloadOnceForChunkError,
58
66
  } from "./chunk-reload.ts";
59
67
 
60
- export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.ts";
68
+ // The two constants, but NOT `hydrateOrMount` — that one is at `@gusnips/react/hydrate`, because
69
+ // it is the package's only `react-dom` import. These come from the contract module, which imports
70
+ // nothing at all, so they are free to everyone: a build script, a browser entry, or a phone.
71
+ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
61
72
 
62
73
  export { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
63
74