@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 +10 -2
- package/dist/describe-error.d.ts +6 -0
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +11 -5
- package/dist/describe-error.js.map +1 -1
- package/dist/hydrate.d.ts +5 -0
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
- package/src/chunk-reload.test.ts +69 -2
- package/src/describe-error.test.ts +36 -0
- package/src/describe-error.ts +18 -4
- package/src/hydrate.ts +5 -0
- package/src/index.ts +18 -7
package/README.md
CHANGED
|
@@ -128,14 +128,22 @@ hurry cannot skip.
|
|
|
128
128
|
|
|
129
129
|
## Also here
|
|
130
130
|
|
|
131
|
-
`
|
|
132
|
-
|
|
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
|
package/dist/describe-error.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/describe-error.js
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA6E1C,sEAAsE;AACtE,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,KAAK,GAAI,OAAwC,CAAC,cAAc,CAAC;IACvE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,CAAoC,EACpC,IAAY,EACZ,MAAc;IAEd,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1F,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACvF,OAAO,CAAC,CAAC,GAAG,MAAM,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;AACrE,CAAC;
|
|
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";
|
package/dist/hydrate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/hydrate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
+
"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
|
},
|
package/src/chunk-reload.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/describe-error.ts
CHANGED
|
@@ -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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
|
|
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
|
|