@gusnips/react 0.5.0 → 0.5.2
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 +5 -0
- package/dist/i18n.d.ts +15 -1
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +3 -2
- package/dist/i18n.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n.test.ts +15 -0
- package/src/i18n.ts +17 -1
- package/dist/chunk-reload.d.ts +0 -61
- package/dist/chunk-reload.d.ts.map +0 -1
- package/dist/chunk-reload.js +0 -92
- package/dist/chunk-reload.js.map +0 -1
package/README.md
CHANGED
|
@@ -159,6 +159,11 @@ hurry cannot skip.
|
|
|
159
159
|
`ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
|
|
160
160
|
`i18nInitOptions`, and `cn`.
|
|
161
161
|
|
|
162
|
+
Pass `storageWriter: "app"` to `i18nInitOptions` when your own language control writes the stored
|
|
163
|
+
locale. Leave it out and the detector writes it, which is right until you offer "follow the
|
|
164
|
+
browser": going back to that means clearing the key and detecting again, and a detector that
|
|
165
|
+
writes on every change puts the language it just detected straight back in. One writer per key.
|
|
166
|
+
|
|
162
167
|
`createAuthStore` is at `@gusnips/react/store` — a zustand store whose `isLoading` starts `false`
|
|
163
168
|
where there is no window. A session bootstrap can only be in flight in a browser, and `true`
|
|
164
169
|
during a build is a wait that never ends: it once shipped a spinner as the indexable body of a
|
package/dist/i18n.d.ts
CHANGED
|
@@ -35,8 +35,22 @@ export interface I18nInitOptions {
|
|
|
35
35
|
* Only one donor had this. The other has the same split-origin layout and the same problem.
|
|
36
36
|
*/
|
|
37
37
|
queryKey?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Who WRITES {@link storageKey}. The detector, by default.
|
|
40
|
+
*
|
|
41
|
+
* Pass `"app"` when your own language control writes it. That is not a preference — it is what
|
|
42
|
+
* a "follow the browser" option costs. Going back to following the browser means clearing the
|
|
43
|
+
* key and calling `changeLanguage(undefined)`, which re-runs detection; a detector that caches
|
|
44
|
+
* then writes the language it just detected straight back into the key it was told to clear.
|
|
45
|
+
* The choice re-pins itself and nothing can reach that state again.
|
|
46
|
+
*
|
|
47
|
+
* Three repos on this stack, three answers: one lets the detector write, one writes it from its
|
|
48
|
+
* own control, and one does both — harmless there only because it offers no "follow the
|
|
49
|
+
* browser" state. So the rule, rather than the default: one writer per key.
|
|
50
|
+
*/
|
|
51
|
+
storageWriter?: "detector" | "app";
|
|
38
52
|
}
|
|
39
|
-
export declare function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, }: I18nInitOptions): {
|
|
53
|
+
export declare function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, storageWriter, }: I18nInitOptions): {
|
|
40
54
|
fallbackLng: string;
|
|
41
55
|
supportedLngs: string[];
|
|
42
56
|
load: "currentOnly";
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;CACpC;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,EACR,aAA0B,GAC3B,EAAE,eAAe;;;;;;;;;;;;;;;;EAwBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAmB/E"}
|
package/dist/i18n.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* product from drifting apart on the details below — each of which is a bug somebody already
|
|
12
12
|
* shipped.
|
|
13
13
|
*/
|
|
14
|
-
export function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, }) {
|
|
14
|
+
export function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, storageWriter = "detector", }) {
|
|
15
15
|
return {
|
|
16
16
|
fallbackLng,
|
|
17
17
|
supportedLngs: [...supportedLngs],
|
|
@@ -21,7 +21,8 @@ export function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryK
|
|
|
21
21
|
order: queryKey
|
|
22
22
|
? ["querystring", "localStorage", "navigator"]
|
|
23
23
|
: ["localStorage", "navigator"],
|
|
24
|
-
|
|
24
|
+
// Read from always, written to only by whoever owns it — see `storageWriter`.
|
|
25
|
+
caches: storageWriter === "detector" ? ["localStorage"] : [],
|
|
25
26
|
...(queryKey ? { lookupQuerystring: queryKey } : {}),
|
|
26
27
|
lookupLocalStorage: storageKey,
|
|
27
28
|
},
|
package/dist/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0CH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,EACR,aAAa,GAAG,UAAU,GACV;IAChB,OAAO;QACL,WAAW;QACX,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;QACjC,IAAI,EAAE,aAAsB;QAC5B,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,yBAAyB;QAChE,SAAS,EAAE;YACT,KAAK,EAAE,QAAQ;gBACb,CAAC,CAAC,CAAC,aAAa,EAAE,cAAc,EAAE,WAAW,CAAC;gBAC9C,CAAC,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC;YACjC,8EAA8E;YAC9E,MAAM,EAAE,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE;YAC5D,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,kBAAkB,EAAE,UAAU;SAC/B;QACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;QAC7B,gGAAgG;QAChG,4FAA4F;QAC5F,4FAA4F;QAC5F,sFAAsF;QACtF,wFAAwF;QACxF,2FAA2F;QAC3F,8FAA8F;KAC/F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAI,SAAY,EAAE,IAA4B;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,UAAU,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EACvF,GAAG,CACJ,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAE9F,MAAM,IAAI,GAAG,CAAC,KAAc,EAAW,EAAE;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YACpC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,IAAI,CAAC,SAAS,CAAM,CAAC;AAC9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"headless"
|
|
61
61
|
],
|
|
62
62
|
"scripts": {
|
|
63
|
-
"build": "tsc",
|
|
63
|
+
"build": "rm -rf dist && tsc",
|
|
64
64
|
"typecheck": "tsc --noEmit",
|
|
65
65
|
"test": "vitest run --passWithNoTests",
|
|
66
66
|
"test:watch": "vitest",
|
package/src/i18n.test.ts
CHANGED
|
@@ -83,4 +83,19 @@ describe("i18nInitOptions", () => {
|
|
|
83
83
|
expect(detection.order).toEqual(["localStorage", "navigator"]);
|
|
84
84
|
expect(detection).not.toHaveProperty("lookupQuerystring");
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
// An app with a "follow the browser" option has to own the key. Getting back to that state
|
|
88
|
+
// means clearing it and re-running detection — and a caching detector writes the language it
|
|
89
|
+
// just detected back into the key it was told to clear, so the choice re-pins itself and the
|
|
90
|
+
// option can never be reached again. The fourth adopter found this; the package had the write
|
|
91
|
+
// hardcoded on.
|
|
92
|
+
it("leaves the key alone when the app is the one writing it", () => {
|
|
93
|
+
const base = { fallbackLng: "en", supportedLngs: ["en"], storageKey: "app.locale" } as const;
|
|
94
|
+
const app = i18nInitOptions({ ...base, storageWriter: "app" });
|
|
95
|
+
|
|
96
|
+
expect(app.detection.caches).toEqual([]);
|
|
97
|
+
// Still READ: the app writes the key, the detector is what notices it.
|
|
98
|
+
expect(app.detection.lookupLocalStorage).toBe("app.locale");
|
|
99
|
+
expect(i18nInitOptions(base).detection.caches).toEqual(["localStorage"]);
|
|
100
|
+
});
|
|
86
101
|
});
|
package/src/i18n.ts
CHANGED
|
@@ -36,6 +36,20 @@ export interface I18nInitOptions {
|
|
|
36
36
|
* Only one donor had this. The other has the same split-origin layout and the same problem.
|
|
37
37
|
*/
|
|
38
38
|
queryKey?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Who WRITES {@link storageKey}. The detector, by default.
|
|
41
|
+
*
|
|
42
|
+
* Pass `"app"` when your own language control writes it. That is not a preference — it is what
|
|
43
|
+
* a "follow the browser" option costs. Going back to following the browser means clearing the
|
|
44
|
+
* key and calling `changeLanguage(undefined)`, which re-runs detection; a detector that caches
|
|
45
|
+
* then writes the language it just detected straight back into the key it was told to clear.
|
|
46
|
+
* The choice re-pins itself and nothing can reach that state again.
|
|
47
|
+
*
|
|
48
|
+
* Three repos on this stack, three answers: one lets the detector write, one writes it from its
|
|
49
|
+
* own control, and one does both — harmless there only because it offers no "follow the
|
|
50
|
+
* browser" state. So the rule, rather than the default: one writer per key.
|
|
51
|
+
*/
|
|
52
|
+
storageWriter?: "detector" | "app";
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export function i18nInitOptions({
|
|
@@ -43,6 +57,7 @@ export function i18nInitOptions({
|
|
|
43
57
|
supportedLngs,
|
|
44
58
|
storageKey,
|
|
45
59
|
queryKey,
|
|
60
|
+
storageWriter = "detector",
|
|
46
61
|
}: I18nInitOptions) {
|
|
47
62
|
return {
|
|
48
63
|
fallbackLng,
|
|
@@ -53,7 +68,8 @@ export function i18nInitOptions({
|
|
|
53
68
|
order: queryKey
|
|
54
69
|
? ["querystring", "localStorage", "navigator"]
|
|
55
70
|
: ["localStorage", "navigator"],
|
|
56
|
-
|
|
71
|
+
// Read from always, written to only by whoever owns it — see `storageWriter`.
|
|
72
|
+
caches: storageWriter === "detector" ? ["localStorage"] : [],
|
|
57
73
|
...(queryKey ? { lookupQuerystring: queryKey } : {}),
|
|
58
74
|
lookupLocalStorage: storageKey,
|
|
59
75
|
},
|
package/dist/chunk-reload.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surviving a deploy that lands while a tab is open.
|
|
3
|
-
*
|
|
4
|
-
* Every app on this stack is a Vite SPA with `lazy()` routes, deployed on push. When a build
|
|
5
|
-
* replaces the one a tab is running, the next navigation asks for a chunk whose hashed name no
|
|
6
|
-
* longer exists — and a static host answers a missing asset with the SPA fallback, so the
|
|
7
|
-
* import gets HTML where it expected a module. React's lazy boundary throws, and the person
|
|
8
|
-
* gets a white screen on a click that worked a minute ago.
|
|
9
|
-
*
|
|
10
|
-
* One repo in the fleet had all of this. The others had none of it, and none of them knew.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* A lazy-chunk fetch failure — recoverable by reloading.
|
|
14
|
-
*
|
|
15
|
-
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is
|
|
16
|
-
* here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
|
|
17
|
-
* dynamically imported module", Safari "Importing a module script failed", webpack's
|
|
18
|
-
* ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
|
|
19
|
-
* which is the one that actually fires on a static host, because the missing `.js` is answered
|
|
20
|
-
* with `index.html`.
|
|
21
|
-
*
|
|
22
|
-
* A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
|
|
23
|
-
* handler can classify the same failure without importing a component.
|
|
24
|
-
*/
|
|
25
|
-
export declare function isChunkLoadError(error: unknown): boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Hard-reload once to pick up the fresh deploy. Returns false when the guard suppressed it.
|
|
28
|
-
*
|
|
29
|
-
* The guard is the part that matters. Reloading on a chunk error is obvious; reloading on a
|
|
30
|
-
* chunk error that the reload does not fix is an infinite loop with the user inside it. At most
|
|
31
|
-
* one reload per minute, so a genuinely missing chunk degrades to the error UI — which can at
|
|
32
|
-
* least say something — instead of flickering forever.
|
|
33
|
-
*
|
|
34
|
-
* `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament,
|
|
35
|
-
* and a stale flag in another tab must not suppress a reload this one needs. Wrapped because
|
|
36
|
-
* storage throws outright when site data is blocked, and a privacy setting must not be the
|
|
37
|
-
* reason somebody cannot recover.
|
|
38
|
-
*/
|
|
39
|
-
export declare function reloadOnceForChunkError(): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Vite's *hint* failure, which is not the same thing and must not be treated as one.
|
|
42
|
-
*
|
|
43
|
-
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
|
|
44
|
-
* When that link fails, the import itself still runs right after and usually succeeds — so this
|
|
45
|
-
* one is worth swallowing, where a failure of the import is the real thing
|
|
46
|
-
* {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
|
|
47
|
-
* for a stylesheet link; script hints never reject.
|
|
48
|
-
*/
|
|
49
|
-
export declare function isPreloadHintFailure(error: unknown): boolean;
|
|
50
|
-
/**
|
|
51
|
-
* Install the `vite:preloadError` listener. Call once, from the browser entry.
|
|
52
|
-
*
|
|
53
|
-
* Vite offers `preventDefault()` here so an app can own a failed asset itself — but taking that
|
|
54
|
-
* deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
|
|
55
|
-
* `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
|
|
56
|
-
* The user gets the crash screen and the developer gets a TypeError with only React frames in
|
|
57
|
-
* it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
|
|
58
|
-
* runs. A real module failure is left to throw, where the error boundary can recognise it.
|
|
59
|
-
*/
|
|
60
|
-
export declare function installPreloadErrorHandler(): void;
|
|
61
|
-
//# sourceMappingURL=chunk-reload.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-reload.d.ts","sourceRoot":"","sources":["../src/chunk-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAWjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD"}
|
package/dist/chunk-reload.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surviving a deploy that lands while a tab is open.
|
|
3
|
-
*
|
|
4
|
-
* Every app on this stack is a Vite SPA with `lazy()` routes, deployed on push. When a build
|
|
5
|
-
* replaces the one a tab is running, the next navigation asks for a chunk whose hashed name no
|
|
6
|
-
* longer exists — and a static host answers a missing asset with the SPA fallback, so the
|
|
7
|
-
* import gets HTML where it expected a module. React's lazy boundary throws, and the person
|
|
8
|
-
* gets a white screen on a click that worked a minute ago.
|
|
9
|
-
*
|
|
10
|
-
* One repo in the fleet had all of this. The others had none of it, and none of them knew.
|
|
11
|
-
*/
|
|
12
|
-
const RELOAD_FLAG_KEY = "frontkit:chunk-reload-at";
|
|
13
|
-
const RELOAD_WINDOW_MS = 60_000;
|
|
14
|
-
/**
|
|
15
|
-
* A lazy-chunk fetch failure — recoverable by reloading.
|
|
16
|
-
*
|
|
17
|
-
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is
|
|
18
|
-
* here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
|
|
19
|
-
* dynamically imported module", Safari "Importing a module script failed", webpack's
|
|
20
|
-
* ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
|
|
21
|
-
* which is the one that actually fires on a static host, because the missing `.js` is answered
|
|
22
|
-
* with `index.html`.
|
|
23
|
-
*
|
|
24
|
-
* A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
|
|
25
|
-
* handler can classify the same failure without importing a component.
|
|
26
|
-
*/
|
|
27
|
-
export function isChunkLoadError(error) {
|
|
28
|
-
if (error instanceof Error && error.name === "ChunkLoadError")
|
|
29
|
-
return true;
|
|
30
|
-
const message = (error instanceof Error ? error.message : String(error) || "").toLowerCase();
|
|
31
|
-
return (message.includes("dynamically imported module") ||
|
|
32
|
-
message.includes("importing a module script failed") ||
|
|
33
|
-
message.includes("mime type") ||
|
|
34
|
-
message.includes("loading chunk"));
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Hard-reload once to pick up the fresh deploy. Returns false when the guard suppressed it.
|
|
38
|
-
*
|
|
39
|
-
* The guard is the part that matters. Reloading on a chunk error is obvious; reloading on a
|
|
40
|
-
* chunk error that the reload does not fix is an infinite loop with the user inside it. At most
|
|
41
|
-
* one reload per minute, so a genuinely missing chunk degrades to the error UI — which can at
|
|
42
|
-
* least say something — instead of flickering forever.
|
|
43
|
-
*
|
|
44
|
-
* `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament,
|
|
45
|
-
* and a stale flag in another tab must not suppress a reload this one needs. Wrapped because
|
|
46
|
-
* storage throws outright when site data is blocked, and a privacy setting must not be the
|
|
47
|
-
* reason somebody cannot recover.
|
|
48
|
-
*/
|
|
49
|
-
export function reloadOnceForChunkError() {
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
try {
|
|
52
|
-
const last = Number(sessionStorage.getItem(RELOAD_FLAG_KEY) ?? 0);
|
|
53
|
-
if (now - last < RELOAD_WINDOW_MS)
|
|
54
|
-
return false;
|
|
55
|
-
sessionStorage.setItem(RELOAD_FLAG_KEY, String(now));
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// Storage blocked — proceed with the reload rather than refusing to recover.
|
|
59
|
-
}
|
|
60
|
-
window.location.reload();
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Vite's *hint* failure, which is not the same thing and must not be treated as one.
|
|
65
|
-
*
|
|
66
|
-
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
|
|
67
|
-
* When that link fails, the import itself still runs right after and usually succeeds — so this
|
|
68
|
-
* one is worth swallowing, where a failure of the import is the real thing
|
|
69
|
-
* {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
|
|
70
|
-
* for a stylesheet link; script hints never reject.
|
|
71
|
-
*/
|
|
72
|
-
export function isPreloadHintFailure(error) {
|
|
73
|
-
return error instanceof Error && error.message.startsWith("Unable to preload CSS for");
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Install the `vite:preloadError` listener. Call once, from the browser entry.
|
|
77
|
-
*
|
|
78
|
-
* Vite offers `preventDefault()` here so an app can own a failed asset itself — but taking that
|
|
79
|
-
* deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
|
|
80
|
-
* `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
|
|
81
|
-
* The user gets the crash screen and the developer gets a TypeError with only React frames in
|
|
82
|
-
* it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
|
|
83
|
-
* runs. A real module failure is left to throw, where the error boundary can recognise it.
|
|
84
|
-
*/
|
|
85
|
-
export function installPreloadErrorHandler() {
|
|
86
|
-
window.addEventListener("vite:preloadError", (event) => {
|
|
87
|
-
const payload = event.payload;
|
|
88
|
-
if (isPreloadHintFailure(payload))
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
//# sourceMappingURL=chunk-reload.js.map
|
package/dist/chunk-reload.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-reload.js","sourceRoot":"","sources":["../src/chunk-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,eAAe,GAAG,0BAA0B,CAAC;AACnD,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,IAAI,CAAC;IAC3E,MAAM,OAAO,GAAG,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7F,OAAO,CACL,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAC;QAC/C,OAAO,CAAC,QAAQ,CAAC,kCAAkC,CAAC;QACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,IAAI,GAAG,GAAG,IAAI,GAAG,gBAAgB;YAAE,OAAO,KAAK,CAAC;QAChD,cAAc,CAAC,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;IACD,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACzB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,2BAA2B,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B;IACxC,MAAM,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;QACrD,MAAM,OAAO,GAAI,KAAuC,CAAC,OAAO,CAAC;QACjE,IAAI,oBAAoB,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,cAAc,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC"}
|