@gusnips/react 0.9.23 → 0.9.25
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 +36 -36
- package/dist/api-client.d.ts +51 -51
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +3 -3
- package/dist/api-error.d.ts +11 -11
- package/dist/api-error.js +11 -11
- package/dist/auth-email.d.ts +40 -0
- package/dist/auth-email.d.ts.map +1 -0
- package/dist/auth-email.js +106 -0
- package/dist/auth-email.js.map +1 -0
- package/dist/auth-store.d.ts +23 -23
- package/dist/auth-store.js +13 -13
- package/dist/copy.d.ts +3 -3
- package/dist/copy.js +3 -3
- package/dist/deploy-recovery.d.ts +40 -40
- package/dist/deploy-recovery.js +40 -40
- package/dist/describe-error.d.ts +68 -68
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +5 -5
- package/dist/error-boundary.d.ts +12 -12
- package/dist/guards.d.ts +15 -15
- package/dist/guards.js +3 -3
- package/dist/hydrate.d.ts +15 -16
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +9 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/i18n.d.ts +15 -16
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +5 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal-path.d.ts +8 -7
- package/dist/internal-path.d.ts.map +1 -1
- package/dist/internal-path.js +12 -9
- package/dist/internal-path.js.map +1 -1
- package/dist/prerender-contract.d.ts +7 -7
- package/dist/prerender-contract.js +7 -7
- package/dist/query.d.ts +16 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +2 -2
- package/dist/query.js.map +1 -1
- package/dist/sse.d.ts +19 -20
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +18 -19
- package/dist/sse.js.map +1 -1
- package/dist/states.d.ts +12 -12
- package/dist/states.d.ts.map +1 -1
- package/dist/supabase-session.d.ts +47 -48
- package/dist/supabase-session.d.ts.map +1 -1
- package/dist/supabase-session.js +35 -35
- package/dist/supabase-session.js.map +1 -1
- package/dist/theme.d.ts +4 -4
- package/dist/theme.js +4 -4
- package/dist/ui/class-name.d.ts +3 -4
- package/dist/ui/class-name.d.ts.map +1 -1
- package/dist/ui/class-name.js +3 -4
- package/dist/ui/class-name.js.map +1 -1
- package/dist/ui/combobox.d.ts +11 -11
- package/dist/ui/combobox.d.ts.map +1 -1
- package/dist/ui/dialog.d.ts +5 -5
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/drawer.d.ts +10 -10
- package/dist/ui/drawer.d.ts.map +1 -1
- package/dist/ui/drawer.js +2 -2
- package/dist/ui/input.d.ts +2 -2
- package/dist/ui/input.js +2 -2
- package/dist/ui/menu.d.ts +8 -8
- package/dist/ui/menu.d.ts.map +1 -1
- package/dist/ui/menu.js +4 -4
- package/dist/ui/menu.js.map +1 -1
- package/dist/ui/select.d.ts +12 -12
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +3 -3
- package/dist/ui/tabs.d.ts +14 -15
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +13 -14
- package/dist/ui/tabs.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.ts +51 -51
- package/src/api-error.ts +11 -11
- package/src/auth-email.test.ts +145 -0
- package/src/auth-email.ts +112 -0
- package/src/auth-store.ts +23 -23
- package/src/copy.ts +3 -3
- package/src/deploy-recovery.ts +40 -40
- package/src/describe-error.ts +68 -68
- package/src/error-boundary.tsx +12 -12
- package/src/guards.tsx +15 -15
- package/src/hydrate.ts +15 -16
- package/src/i18n.ts +15 -16
- package/src/index.ts +2 -0
- package/src/internal-path.test.ts +1 -0
- package/src/internal-path.ts +12 -9
- package/src/prerender-contract.ts +7 -7
- package/src/query.ts +16 -17
- package/src/sse.ts +19 -20
- package/src/states.ts +12 -12
- package/src/supabase-session.ts +46 -47
- package/src/theme.ts +4 -4
- package/src/ui/class-name.ts +3 -4
- package/src/ui/combobox.tsx +11 -11
- package/src/ui/dialog.tsx +5 -5
- package/src/ui/drawer.tsx +10 -10
- package/src/ui/input.tsx +2 -2
- package/src/ui/menu.tsx +8 -8
- package/src/ui/select.tsx +12 -12
- package/src/ui/tabs.tsx +14 -15
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The email a person has typed during sign-in, kept so the next auth screen opens with it filled.
|
|
3
|
+
*
|
|
4
|
+
* Sign-in, "use a code instead", "forgot password" and "create account" are separate screens, and
|
|
5
|
+
* each one used to start with an empty email field. Someone who typed their address, then chose
|
|
6
|
+
* another way in, typed it again. The fix is the same everywhere, so it lives here once.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <input name="email" type="email" defaultValue={recallAuthEmail()}
|
|
10
|
+
* onChange={(event) => rememberAuthEmail(event.currentTarget.value)} />
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* - **Not in the URL.** A query string rides in `Referer`, in access logs, in history and in every
|
|
14
|
+
* analytics tool that records page addresses. An email is personal data and has no business there.
|
|
15
|
+
* - **`localStorage`, not `sessionStorage`.** An emailed link opens in a new tab, and a new tab
|
|
16
|
+
* does not inherit `sessionStorage`. The person who confirms their account from the mail lands
|
|
17
|
+
* on sign-in in that new tab, which is the screen that most needs the address.
|
|
18
|
+
* - **Thirty minutes, sliding.** Long enough to read a code in a mail client and come back; short
|
|
19
|
+
* enough that a shared computer forgets. Every keystroke renews it, and a value found expired is
|
|
20
|
+
* deleted when it is read, so it does not sit on disk afterwards.
|
|
21
|
+
* - **Memory behind it.** A browser that blocks site data throws on `localStorage` itself, and
|
|
22
|
+
* React Native has none. The value then lasts until the page (or the app) is left, which still
|
|
23
|
+
* covers moving between screens.
|
|
24
|
+
*
|
|
25
|
+
* Read it once when a screen opens, as a default value. It is not a live store: nothing re-renders
|
|
26
|
+
* when it changes, and a prerendered page must read it after hydration, never while rendering.
|
|
27
|
+
* Never put a password, a code or a token here. Call `forgetAuthEmail()` once a session exists, so
|
|
28
|
+
* the next person at this browser starts empty.
|
|
29
|
+
*/
|
|
30
|
+
const KEY = "frontkit.auth-email";
|
|
31
|
+
const TTL_MS = 30 * 60 * 1000;
|
|
32
|
+
/** The longest an address can be (RFC 5321). A longer value is not one, and is not kept. */
|
|
33
|
+
const MAX_LENGTH = 254;
|
|
34
|
+
let memory = null;
|
|
35
|
+
function isSaved(value) {
|
|
36
|
+
return (typeof value === "object" &&
|
|
37
|
+
value !== null &&
|
|
38
|
+
"email" in value &&
|
|
39
|
+
typeof value.email === "string" &&
|
|
40
|
+
"at" in value &&
|
|
41
|
+
typeof value.at === "number");
|
|
42
|
+
}
|
|
43
|
+
function readStored() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = globalThis.localStorage?.getItem(KEY);
|
|
46
|
+
if (!raw)
|
|
47
|
+
return null;
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
return isSaved(parsed) ? parsed : null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Storage and memory are written together, so they only differ when one of them missed a write:
|
|
57
|
+
* storage that is full keeps an older address while memory holds the new one, and a second tab
|
|
58
|
+
* writes storage while this tab's memory goes stale. The newer of the two is the one the person
|
|
59
|
+
* typed last.
|
|
60
|
+
*/
|
|
61
|
+
function newest() {
|
|
62
|
+
const stored = readStored();
|
|
63
|
+
if (!stored || !memory)
|
|
64
|
+
return stored ?? memory;
|
|
65
|
+
return stored.at >= memory.at ? stored : memory;
|
|
66
|
+
}
|
|
67
|
+
/** The address typed earlier in this sign-in, or an empty string when there is none. */
|
|
68
|
+
export function recallAuthEmail() {
|
|
69
|
+
const saved = newest();
|
|
70
|
+
if (!saved)
|
|
71
|
+
return "";
|
|
72
|
+
if (Date.now() - saved.at > TTL_MS) {
|
|
73
|
+
forgetAuthEmail();
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
return saved.email;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Keep what the person has typed. Call it from the email field's change handler, so every screen
|
|
80
|
+
* they reach next can open with it. The value is stored as typed: trim it where you submit it.
|
|
81
|
+
* An empty value forgets.
|
|
82
|
+
*/
|
|
83
|
+
export function rememberAuthEmail(value) {
|
|
84
|
+
if (value === "")
|
|
85
|
+
return forgetAuthEmail();
|
|
86
|
+
if (value.length > MAX_LENGTH)
|
|
87
|
+
return;
|
|
88
|
+
memory = { email: value, at: Date.now() };
|
|
89
|
+
try {
|
|
90
|
+
globalThis.localStorage?.setItem(KEY, JSON.stringify(memory));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Blocked or full: the memory copy above carries it until the page is left.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Drop it. Call this once a session exists, so the next person at this browser starts empty. */
|
|
97
|
+
export function forgetAuthEmail() {
|
|
98
|
+
memory = null;
|
|
99
|
+
try {
|
|
100
|
+
globalThis.localStorage?.removeItem(KEY);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Blocked: nothing was stored, so there is nothing to drop.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=auth-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-email.js","sourceRoot":"","sources":["../src/auth-email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,GAAG,GAAG,qBAAqB,CAAC;AAClC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC9B,4FAA4F;AAC5F,MAAM,UAAU,GAAG,GAAG,CAAC;AAOvB,IAAI,MAAM,GAAiB,IAAI,CAAC;AAEhC,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,IAAI,KAAK;QAChB,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,IAAI,IAAI,KAAK;QACb,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,CAC7B,CAAC;AACJ,CAAC;AAED,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,IAAI,MAAM,CAAC;IAChD,OAAO,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAClD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACnC,eAAe,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,eAAe,EAAE,CAAC;IAC3C,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO;IACtC,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC1C,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;IAC9E,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,IAAI,CAAC;IACd,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;AACH,CAAC"}
|
package/dist/auth-store.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ import { type StoreApi, type UseBoundStore } from "zustand";
|
|
|
2
2
|
/**
|
|
3
3
|
* Session FLAGS only.
|
|
4
4
|
*
|
|
5
|
-
* Who the person is
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Who the person is, including their plan, staff bit and suspension status, comes from `GET
|
|
6
|
+
* /auth/me` through react-query, never from here. Mirroring server state in a client store is how
|
|
7
|
+
* two sources of truth start disagreeing, and the one that is wrong is always the one on screen.
|
|
8
|
+
* Three repos wrote this same store; two of them wrote that same warning in a comment.
|
|
9
9
|
*
|
|
10
10
|
* `TUser` stays generic because it is the one part that differs: one product carries an
|
|
11
|
-
* anonymous-browsing flag, another does not. Keep it to what a GUARD needs
|
|
12
|
-
*
|
|
11
|
+
* anonymous-browsing flag, another does not. Keep it to what a GUARD needs. All three donors had an
|
|
12
|
+
* id and an email.
|
|
13
13
|
*/
|
|
14
14
|
export interface AuthState<TUser> {
|
|
15
15
|
user: TUser | null;
|
|
@@ -20,10 +20,10 @@ export interface AuthState<TUser> {
|
|
|
20
20
|
*/
|
|
21
21
|
isLoading: boolean;
|
|
22
22
|
/**
|
|
23
|
-
* Set (or clear) the signed-in user. This ENDS the loading state
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Set (or clear) the signed-in user. This ENDS the loading state, because the bootstrap was
|
|
24
|
+
* waiting to learn who they are. Two of the three donors left `isLoading` alone here and relied
|
|
25
|
+
* on a separate `setLoading(false)`; forgetting that call leaves every guard spinning, so the
|
|
26
|
+
* safe default is to do it in one write.
|
|
27
27
|
*/
|
|
28
28
|
setUser: (user: TUser | null) => void;
|
|
29
29
|
setLoading: (isLoading: boolean) => void;
|
|
@@ -36,12 +36,12 @@ export interface AuthState<TUser> {
|
|
|
36
36
|
* The whole reason this is a factory and not a store: `isLoading` must start FALSE where there
|
|
37
37
|
* is no window, and only a factory can decide that at the call site rather than at import.
|
|
38
38
|
*
|
|
39
|
-
* A session bootstrap can only be in flight in a browser. The BUILD renders this app to files
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* A session bootstrap can only be in flight in a browser. The BUILD renders this app to files with
|
|
40
|
+
* no window at all, so `true` there is a wait that never ends. One donor shipped a route guard
|
|
41
|
+
* holding its loading screen forever, and prerendered a public page as 1,174 bytes of
|
|
42
|
+
* `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be found.
|
|
43
|
+
* With no browser there is no session and never will be, which is exactly the state a first-time
|
|
44
|
+
* visitor arrives in, so that is what the page should render.
|
|
45
45
|
*
|
|
46
46
|
* Only one of the three donors knew this. It is invariant 7.
|
|
47
47
|
*/
|
|
@@ -59,13 +59,13 @@ type AuthSet<TUser, TExtra> = (partial: (TExtra & Partial<AuthState<TUser>>) | P
|
|
|
59
59
|
/**
|
|
60
60
|
* The session flags as a plain object, for a product whose store needs more than them.
|
|
61
61
|
*
|
|
62
|
-
* `createAuthStore` owns its `create()` call, which means it owns the whole store: a product
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
62
|
+
* `createAuthStore` owns its `create()` call, which means it owns the whole store: a product cannot
|
|
63
|
+
* wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that met it had
|
|
64
|
+
* a superset and neither could use it. One holds a remember-me choice, a profile row and the
|
|
65
|
+
* sign-in methods themselves, under `persist`. The other adds `isAnonymous`, which is the reason
|
|
66
|
+
* this takes a `derive` function rather than just letting the caller spread extra keys in:
|
|
67
|
+
* `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and `clear`, which are
|
|
68
|
+
* exactly the two writes the product does not own.
|
|
69
69
|
*
|
|
70
70
|
* The caller keeps `create`, so middleware, extra actions and the store's own name stay theirs:
|
|
71
71
|
*
|
package/dist/auth-store.js
CHANGED
|
@@ -5,12 +5,12 @@ import { create } from "zustand";
|
|
|
5
5
|
* The whole reason this is a factory and not a store: `isLoading` must start FALSE where there
|
|
6
6
|
* is no window, and only a factory can decide that at the call site rather than at import.
|
|
7
7
|
*
|
|
8
|
-
* A session bootstrap can only be in flight in a browser. The BUILD renders this app to files
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* A session bootstrap can only be in flight in a browser. The BUILD renders this app to files with
|
|
9
|
+
* no window at all, so `true` there is a wait that never ends. One donor shipped a route guard
|
|
10
|
+
* holding its loading screen forever, and prerendered a public page as 1,174 bytes of
|
|
11
|
+
* `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be found.
|
|
12
|
+
* With no browser there is no session and never will be, which is exactly the state a first-time
|
|
13
|
+
* visitor arrives in, so that is what the page should render.
|
|
14
14
|
*
|
|
15
15
|
* Only one of the three donors knew this. It is invariant 7.
|
|
16
16
|
*/
|
|
@@ -20,13 +20,13 @@ export function createAuthStore() {
|
|
|
20
20
|
/**
|
|
21
21
|
* The session flags as a plain object, for a product whose store needs more than them.
|
|
22
22
|
*
|
|
23
|
-
* `createAuthStore` owns its `create()` call, which means it owns the whole store: a product
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* `createAuthStore` owns its `create()` call, which means it owns the whole store: a product cannot
|
|
24
|
+
* wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that met it had
|
|
25
|
+
* a superset and neither could use it. One holds a remember-me choice, a profile row and the
|
|
26
|
+
* sign-in methods themselves, under `persist`. The other adds `isAnonymous`, which is the reason
|
|
27
|
+
* this takes a `derive` function rather than just letting the caller spread extra keys in:
|
|
28
|
+
* `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and `clear`, which are
|
|
29
|
+
* exactly the two writes the product does not own.
|
|
30
30
|
*
|
|
31
31
|
* The caller keeps `create`, so middleware, extra actions and the store's own name stay theirs:
|
|
32
32
|
*
|
package/dist/copy.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export interface CopyOptions {
|
|
|
8
8
|
failedMs?: number;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* A copy with an outcome the reader can see.
|
|
12
|
-
* and copy it yourself", in a label or a toast
|
|
13
|
-
* needs `aria-live="polite"` to be heard.
|
|
11
|
+
* A copy with an outcome the reader can see. You choose what it says, such as "Copied" or "Select
|
|
12
|
+
* it and copy it yourself", in a label or a toast. You also choose how to announce it: a label that
|
|
13
|
+
* changes needs `aria-live="polite"` to be heard.
|
|
14
14
|
*
|
|
15
15
|
* The outcome goes back to `"idle"` on its own. A "Copied" that never clears makes the second copy
|
|
16
16
|
* look like it did not take.
|
package/dist/copy.js
CHANGED
|
@@ -18,9 +18,9 @@ export async function copyText(text) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* A copy with an outcome the reader can see.
|
|
22
|
-
* and copy it yourself", in a label or a toast
|
|
23
|
-
* needs `aria-live="polite"` to be heard.
|
|
21
|
+
* A copy with an outcome the reader can see. You choose what it says, such as "Copied" or "Select
|
|
22
|
+
* it and copy it yourself", in a label or a toast. You also choose how to announce it: a label that
|
|
23
|
+
* changes needs `aria-live="polite"` to be heard.
|
|
24
24
|
*
|
|
25
25
|
* The outcome goes back to `"idle"` on its own. A "Copied" that never clears makes the second copy
|
|
26
26
|
* look like it did not take.
|
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
* every hashed asset underneath whatever tabs are already open, and those tabs go on running
|
|
6
6
|
* yesterday's bundle. It surfaces two ways, and only one of them looks like a missing file:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* 1. The next navigation asks for a chunk whose hashed name is gone. React's lazy boundary throws,
|
|
9
|
+
* and the person gets a white screen on a click that worked a minute ago. 2. The old bundle reads a
|
|
10
|
+
* field the API has since renamed, and throws. That one arrives as a plain `TypeError` with nothing
|
|
11
|
+
* in it about a deploy: no file, no hash, no signal.
|
|
12
12
|
*
|
|
13
13
|
* So there are two detectors here and they are not interchangeable. {@link isChunkLoadError}
|
|
14
|
-
* matches the message, which is all you have for a failure that never reaches a boundary.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* matches the message, which is all you have for a failure that never reaches a boundary. {@link
|
|
15
|
+
* isStaleBuild} asks the server for evidence rather than a guess. It is the only one that catches
|
|
16
|
+
* case 2 at all.
|
|
17
17
|
*
|
|
18
18
|
* **The deepest fix is not in this file, and it is worth more than everything in it.** A static
|
|
19
|
-
* host answers a missing `/assets/x.js` with the SPA fallback
|
|
20
|
-
* `text/html
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
19
|
+
* host answers a missing `/assets/x.js` with the SPA fallback, `index.html`, at a 200, as
|
|
20
|
+
* `text/html`. That is what the browser's "MIME type text/html" module-script refusal actually is.
|
|
21
|
+
* Hosts then match cache rules against the REQUEST PATH rather than the outcome, so the `immutable,
|
|
22
|
+
* max-age=31536000` rule written for hashed assets gets applied to that HTML body: one poisoned
|
|
23
|
+
* asset URL, at the edge and in every browser that touched it, for a year. One adopter removed the
|
|
24
|
+
* cause instead of recovering from it: no blanket fallback over `/assets/`, and an edge function
|
|
25
|
+
* that refuses to answer an asset path with `text/html`. That turns a dead chunk into a real,
|
|
26
|
+
* uncacheable 404 and lets everything below be recovery rather than the plan. A package cannot ship
|
|
27
|
+
* that; it can refuse to let it go unwritten.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
30
|
* One stamp for every reason to reload, which is the part that is easy to get wrong.
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
*/
|
|
41
41
|
export declare const RELOAD_GUARD_KEY = "frontkit:reload-at";
|
|
42
42
|
/**
|
|
43
|
-
* A lazy-chunk fetch failure
|
|
43
|
+
* A lazy-chunk fetch failure that a reload can fix.
|
|
44
44
|
*
|
|
45
|
-
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
45
|
+
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is here:
|
|
46
|
+
* Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading dynamically
|
|
47
|
+
* imported module", Safari "Importing a module script failed", webpack's ChunkLoadError and
|
|
48
|
+
* "Loading chunk N failed", and the SPA-fallback "MIME type" refusal. The last one that fires on a
|
|
49
|
+
* static host that has not fixed the cause above, because the missing `.js` is answered with
|
|
50
|
+
* `index.html`.
|
|
51
51
|
*
|
|
52
52
|
* A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
|
|
53
53
|
* handler can classify the same failure without importing a component.
|
|
@@ -56,13 +56,13 @@ export declare function isChunkLoadError(error: unknown): boolean;
|
|
|
56
56
|
/**
|
|
57
57
|
* Is the build this tab is running still the one being served?
|
|
58
58
|
*
|
|
59
|
-
* Asked of the server, so the answer is evidence rather than a guess.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* Asked of the server, so the answer is evidence rather than a guess. An offline request, a non-200
|
|
60
|
+
* response, a captive portal or a timed-out probe resolves to `false`: dressing a genuine bug up as
|
|
61
|
+
* an update hides it from the user and from us.
|
|
62
62
|
*
|
|
63
63
|
* It asks for the PAGE, not for the asset, and that is not the obvious choice. A missing asset
|
|
64
64
|
* looks like the direct question, but a CDN commonly serves assets with a long `s-maxage`, so a
|
|
65
|
-
* bundle a deploy retired an hour ago still answers 200 from the edge
|
|
65
|
+
* bundle a deploy retired an hour ago still answers 200 from the edge. It would report "current"
|
|
66
66
|
* during exactly the window when skew is most likely. The page is the one URL that cannot lie: it
|
|
67
67
|
* ships `max-age=0, must-revalidate` and never enters the edge cache.
|
|
68
68
|
*/
|
|
@@ -73,9 +73,9 @@ export declare function isStaleBuild(): Promise<boolean>;
|
|
|
73
73
|
* rather than watching the same attempt loop.
|
|
74
74
|
*
|
|
75
75
|
* The guard is the part that matters. Reloading on a failure is obvious; reloading on a failure
|
|
76
|
-
* that the reload does not fix is an infinite loop with the user inside it. At most one reload
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* that the reload does not fix is an infinite loop with the user inside it. At most one reload per
|
|
77
|
+
* minute, so a genuinely broken deploy shows the error screen instead of flickering forever. That
|
|
78
|
+
* screen can at least say something.
|
|
79
79
|
*
|
|
80
80
|
* `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament, and
|
|
81
81
|
* a stale stamp in another tab must not suppress a reload this one needs. Wrapped because storage
|
|
@@ -86,22 +86,22 @@ export declare function reloadOnce(): boolean;
|
|
|
86
86
|
/**
|
|
87
87
|
* Vite's *hint* failure, which is not the same thing and must not be treated as one.
|
|
88
88
|
*
|
|
89
|
-
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
89
|
+
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it. When
|
|
90
|
+
* that link fails, the import itself still runs right after and usually succeeds, so this one is
|
|
91
|
+
* worth swallowing, where a failure of the import is the real thing {@link isChunkLoadError}
|
|
92
|
+
* matches. Vite builds this message in exactly one place and only ever for a stylesheet link;
|
|
93
|
+
* script hints never reject.
|
|
94
94
|
*/
|
|
95
95
|
export declare function isPreloadHintFailure(error: unknown): boolean;
|
|
96
96
|
/**
|
|
97
97
|
* Install the `vite:preloadError` listener. Call once, from the browser entry.
|
|
98
98
|
*
|
|
99
|
-
* Vite offers `preventDefault()` here so an app can own a failed asset itself
|
|
100
|
-
*
|
|
99
|
+
* Vite offers `preventDefault()` here so an app can own a failed asset itself, but taking that deal
|
|
100
|
+
* unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
|
|
101
101
|
* `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
|
|
102
|
-
* The user gets the crash screen and the developer gets a TypeError with only React frames in
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* The user gets the crash screen and the developer gets a TypeError with only React frames in it,
|
|
103
|
+
* naming no chunk. So it is taken ONLY for a failed preload hint, where the import still runs. A
|
|
104
|
+
* real module failure is left to throw, where the error boundary can recognise it.
|
|
105
105
|
*/
|
|
106
106
|
export declare function installPreloadErrorHandler(): void;
|
|
107
107
|
//# sourceMappingURL=deploy-recovery.d.ts.map
|
package/dist/deploy-recovery.js
CHANGED
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
* every hashed asset underneath whatever tabs are already open, and those tabs go on running
|
|
6
6
|
* yesterday's bundle. It surfaces two ways, and only one of them looks like a missing file:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* 1. The next navigation asks for a chunk whose hashed name is gone. React's lazy boundary throws,
|
|
9
|
+
* and the person gets a white screen on a click that worked a minute ago. 2. The old bundle reads a
|
|
10
|
+
* field the API has since renamed, and throws. That one arrives as a plain `TypeError` with nothing
|
|
11
|
+
* in it about a deploy: no file, no hash, no signal.
|
|
12
12
|
*
|
|
13
13
|
* So there are two detectors here and they are not interchangeable. {@link isChunkLoadError}
|
|
14
|
-
* matches the message, which is all you have for a failure that never reaches a boundary.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* matches the message, which is all you have for a failure that never reaches a boundary. {@link
|
|
15
|
+
* isStaleBuild} asks the server for evidence rather than a guess. It is the only one that catches
|
|
16
|
+
* case 2 at all.
|
|
17
17
|
*
|
|
18
18
|
* **The deepest fix is not in this file, and it is worth more than everything in it.** A static
|
|
19
|
-
* host answers a missing `/assets/x.js` with the SPA fallback
|
|
20
|
-
* `text/html
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
19
|
+
* host answers a missing `/assets/x.js` with the SPA fallback, `index.html`, at a 200, as
|
|
20
|
+
* `text/html`. That is what the browser's "MIME type text/html" module-script refusal actually is.
|
|
21
|
+
* Hosts then match cache rules against the REQUEST PATH rather than the outcome, so the `immutable,
|
|
22
|
+
* max-age=31536000` rule written for hashed assets gets applied to that HTML body: one poisoned
|
|
23
|
+
* asset URL, at the edge and in every browser that touched it, for a year. One adopter removed the
|
|
24
|
+
* cause instead of recovering from it: no blanket fallback over `/assets/`, and an edge function
|
|
25
|
+
* that refuses to answer an asset path with `text/html`. That turns a dead chunk into a real,
|
|
26
|
+
* uncacheable 404 and lets everything below be recovery rather than the plan. A package cannot ship
|
|
27
|
+
* that; it can refuse to let it go unwritten.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
30
|
* One stamp for every reason to reload, which is the part that is easy to get wrong.
|
|
@@ -44,14 +44,14 @@ const RELOAD_WINDOW_MS = 60_000;
|
|
|
44
44
|
* land on the real error screen instead of an endless spinner. */
|
|
45
45
|
const PROBE_TIMEOUT_MS = 4_000;
|
|
46
46
|
/**
|
|
47
|
-
* A lazy-chunk fetch failure
|
|
47
|
+
* A lazy-chunk fetch failure that a reload can fix.
|
|
48
48
|
*
|
|
49
|
-
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* Matched by message because no browser gives it a shared type. Every phrasing in the wild is here:
|
|
50
|
+
* Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading dynamically
|
|
51
|
+
* imported module", Safari "Importing a module script failed", webpack's ChunkLoadError and
|
|
52
|
+
* "Loading chunk N failed", and the SPA-fallback "MIME type" refusal. The last one that fires on a
|
|
53
|
+
* static host that has not fixed the cause above, because the missing `.js` is answered with
|
|
54
|
+
* `index.html`.
|
|
55
55
|
*
|
|
56
56
|
* A standalone predicate rather than a method on the boundary, so a global `unhandledrejection`
|
|
57
57
|
* handler can classify the same failure without importing a component.
|
|
@@ -86,13 +86,13 @@ function runningEntry() {
|
|
|
86
86
|
/**
|
|
87
87
|
* Is the build this tab is running still the one being served?
|
|
88
88
|
*
|
|
89
|
-
* Asked of the server, so the answer is evidence rather than a guess.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* Asked of the server, so the answer is evidence rather than a guess. An offline request, a non-200
|
|
90
|
+
* response, a captive portal or a timed-out probe resolves to `false`: dressing a genuine bug up as
|
|
91
|
+
* an update hides it from the user and from us.
|
|
92
92
|
*
|
|
93
93
|
* It asks for the PAGE, not for the asset, and that is not the obvious choice. A missing asset
|
|
94
94
|
* looks like the direct question, but a CDN commonly serves assets with a long `s-maxage`, so a
|
|
95
|
-
* bundle a deploy retired an hour ago still answers 200 from the edge
|
|
95
|
+
* bundle a deploy retired an hour ago still answers 200 from the edge. It would report "current"
|
|
96
96
|
* during exactly the window when skew is most likely. The page is the one URL that cannot lie: it
|
|
97
97
|
* ships `max-age=0, must-revalidate` and never enters the edge cache.
|
|
98
98
|
*/
|
|
@@ -119,9 +119,9 @@ export async function isStaleBuild() {
|
|
|
119
119
|
* rather than watching the same attempt loop.
|
|
120
120
|
*
|
|
121
121
|
* The guard is the part that matters. Reloading on a failure is obvious; reloading on a failure
|
|
122
|
-
* that the reload does not fix is an infinite loop with the user inside it. At most one reload
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* that the reload does not fix is an infinite loop with the user inside it. At most one reload per
|
|
123
|
+
* minute, so a genuinely broken deploy shows the error screen instead of flickering forever. That
|
|
124
|
+
* screen can at least say something.
|
|
125
125
|
*
|
|
126
126
|
* `sessionStorage` and not `localStorage`: the guard is about this tab's current predicament, and
|
|
127
127
|
* a stale stamp in another tab must not suppress a reload this one needs. Wrapped because storage
|
|
@@ -145,11 +145,11 @@ export function reloadOnce() {
|
|
|
145
145
|
/**
|
|
146
146
|
* Vite's *hint* failure, which is not the same thing and must not be treated as one.
|
|
147
147
|
*
|
|
148
|
-
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
148
|
+
* Vite injects a speculative `<link rel="stylesheet">` for a chunk's CSS before importing it. When
|
|
149
|
+
* that link fails, the import itself still runs right after and usually succeeds, so this one is
|
|
150
|
+
* worth swallowing, where a failure of the import is the real thing {@link isChunkLoadError}
|
|
151
|
+
* matches. Vite builds this message in exactly one place and only ever for a stylesheet link;
|
|
152
|
+
* script hints never reject.
|
|
153
153
|
*/
|
|
154
154
|
export function isPreloadHintFailure(error) {
|
|
155
155
|
return error instanceof Error && error.message.startsWith("Unable to preload CSS for");
|
|
@@ -157,12 +157,12 @@ export function isPreloadHintFailure(error) {
|
|
|
157
157
|
/**
|
|
158
158
|
* Install the `vite:preloadError` listener. Call once, from the browser entry.
|
|
159
159
|
*
|
|
160
|
-
* Vite offers `preventDefault()` here so an app can own a failed asset itself
|
|
161
|
-
*
|
|
160
|
+
* Vite offers `preventDefault()` here so an app can own a failed asset itself, but taking that deal
|
|
161
|
+
* unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
|
|
162
162
|
* `baseModule().catch(handlePreloadError)`), and `React.lazy` then reads `.default` off nothing.
|
|
163
|
-
* The user gets the crash screen and the developer gets a TypeError with only React frames in
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* The user gets the crash screen and the developer gets a TypeError with only React frames in it,
|
|
164
|
+
* naming no chunk. So it is taken ONLY for a failed preload hint, where the import still runs. A
|
|
165
|
+
* real module failure is left to throw, where the error boundary can recognise it.
|
|
166
166
|
*/
|
|
167
167
|
export function installPreloadErrorHandler() {
|
|
168
168
|
window.addEventListener("vite:preloadError", (event) => {
|