@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.
Files changed (110) hide show
  1. package/README.md +36 -36
  2. package/dist/api-client.d.ts +51 -51
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +3 -3
  5. package/dist/api-error.d.ts +11 -11
  6. package/dist/api-error.js +11 -11
  7. package/dist/auth-email.d.ts +40 -0
  8. package/dist/auth-email.d.ts.map +1 -0
  9. package/dist/auth-email.js +106 -0
  10. package/dist/auth-email.js.map +1 -0
  11. package/dist/auth-store.d.ts +23 -23
  12. package/dist/auth-store.js +13 -13
  13. package/dist/copy.d.ts +3 -3
  14. package/dist/copy.js +3 -3
  15. package/dist/deploy-recovery.d.ts +40 -40
  16. package/dist/deploy-recovery.js +40 -40
  17. package/dist/describe-error.d.ts +68 -68
  18. package/dist/describe-error.d.ts.map +1 -1
  19. package/dist/describe-error.js +5 -5
  20. package/dist/error-boundary.d.ts +12 -12
  21. package/dist/guards.d.ts +15 -15
  22. package/dist/guards.js +3 -3
  23. package/dist/hydrate.d.ts +15 -16
  24. package/dist/hydrate.d.ts.map +1 -1
  25. package/dist/hydrate.js +9 -9
  26. package/dist/hydrate.js.map +1 -1
  27. package/dist/i18n.d.ts +15 -16
  28. package/dist/i18n.d.ts.map +1 -1
  29. package/dist/i18n.js +5 -6
  30. package/dist/i18n.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal-path.d.ts +8 -7
  36. package/dist/internal-path.d.ts.map +1 -1
  37. package/dist/internal-path.js +12 -9
  38. package/dist/internal-path.js.map +1 -1
  39. package/dist/prerender-contract.d.ts +7 -7
  40. package/dist/prerender-contract.js +7 -7
  41. package/dist/query.d.ts +16 -17
  42. package/dist/query.d.ts.map +1 -1
  43. package/dist/query.js +2 -2
  44. package/dist/query.js.map +1 -1
  45. package/dist/sse.d.ts +19 -20
  46. package/dist/sse.d.ts.map +1 -1
  47. package/dist/sse.js +18 -19
  48. package/dist/sse.js.map +1 -1
  49. package/dist/states.d.ts +12 -12
  50. package/dist/states.d.ts.map +1 -1
  51. package/dist/supabase-session.d.ts +47 -48
  52. package/dist/supabase-session.d.ts.map +1 -1
  53. package/dist/supabase-session.js +35 -35
  54. package/dist/supabase-session.js.map +1 -1
  55. package/dist/theme.d.ts +4 -4
  56. package/dist/theme.js +4 -4
  57. package/dist/ui/class-name.d.ts +3 -4
  58. package/dist/ui/class-name.d.ts.map +1 -1
  59. package/dist/ui/class-name.js +3 -4
  60. package/dist/ui/class-name.js.map +1 -1
  61. package/dist/ui/combobox.d.ts +11 -11
  62. package/dist/ui/combobox.d.ts.map +1 -1
  63. package/dist/ui/dialog.d.ts +5 -5
  64. package/dist/ui/dialog.js +2 -2
  65. package/dist/ui/drawer.d.ts +10 -10
  66. package/dist/ui/drawer.d.ts.map +1 -1
  67. package/dist/ui/drawer.js +2 -2
  68. package/dist/ui/input.d.ts +2 -2
  69. package/dist/ui/input.js +2 -2
  70. package/dist/ui/menu.d.ts +8 -8
  71. package/dist/ui/menu.d.ts.map +1 -1
  72. package/dist/ui/menu.js +4 -4
  73. package/dist/ui/menu.js.map +1 -1
  74. package/dist/ui/select.d.ts +12 -12
  75. package/dist/ui/select.d.ts.map +1 -1
  76. package/dist/ui/select.js +3 -3
  77. package/dist/ui/tabs.d.ts +14 -15
  78. package/dist/ui/tabs.d.ts.map +1 -1
  79. package/dist/ui/tabs.js +13 -14
  80. package/dist/ui/tabs.js.map +1 -1
  81. package/package.json +1 -1
  82. package/src/api-client.ts +51 -51
  83. package/src/api-error.ts +11 -11
  84. package/src/auth-email.test.ts +145 -0
  85. package/src/auth-email.ts +112 -0
  86. package/src/auth-store.ts +23 -23
  87. package/src/copy.ts +3 -3
  88. package/src/deploy-recovery.ts +40 -40
  89. package/src/describe-error.ts +68 -68
  90. package/src/error-boundary.tsx +12 -12
  91. package/src/guards.tsx +15 -15
  92. package/src/hydrate.ts +15 -16
  93. package/src/i18n.ts +15 -16
  94. package/src/index.ts +2 -0
  95. package/src/internal-path.test.ts +1 -0
  96. package/src/internal-path.ts +12 -9
  97. package/src/prerender-contract.ts +7 -7
  98. package/src/query.ts +16 -17
  99. package/src/sse.ts +19 -20
  100. package/src/states.ts +12 -12
  101. package/src/supabase-session.ts +46 -47
  102. package/src/theme.ts +4 -4
  103. package/src/ui/class-name.ts +3 -4
  104. package/src/ui/combobox.tsx +11 -11
  105. package/src/ui/dialog.tsx +5 -5
  106. package/src/ui/drawer.tsx +10 -10
  107. package/src/ui/input.tsx +2 -2
  108. package/src/ui/menu.tsx +8 -8
  109. package/src/ui/select.tsx +12 -12
  110. 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"}
@@ -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 — their plan, their staff bit, whether they are suspended — comes from
6
- * `GET /auth/me` through react-query, never from here. Mirroring server state in a client store
7
- * is how two sources of truth start disagreeing, and the one that is wrong is always the one on
8
- * screen. Three repos wrote this same store; two of them wrote that same warning in a comment.
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 — an id and an
12
- * email is what all three donors had.
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 — knowing who they are is
24
- * what the bootstrap was waiting for. Two of the three donors left `isLoading` alone here and
25
- * relied on a separate `setLoading(false)`; forgetting that call leaves every guard spinning,
26
- * so the safe default is to do it in one write.
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
- * with no window at all, so `true` there is a wait that never ends — one donor shipped a route
41
- * guard 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
43
- * found. With no browser there is no session and never will be, which is exactly the state a
44
- * first-time visitor arrives in, so that is what the page should render.
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
- * cannot wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that
64
- * met it had a superset and neither could use it. One holds a remember-me choice, a profile row
65
- * and the sign-in methods themselves, under `persist`. The other adds `isAnonymous` — and that
66
- * one is the reason this takes a `derive` function rather than just letting the caller spread
67
- * extra keys in: `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and
68
- * `clear`, which are exactly the two writes the product does not own.
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
  *
@@ -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
- * with no window at all, so `true` there is a wait that never ends — one donor shipped a route
10
- * guard 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
12
- * found. With no browser there is no session and never will be, which is exactly the state a
13
- * first-time visitor arrives in, so that is what the page should render.
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
- * cannot wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that
25
- * met it had a superset and neither could use it. One holds a remember-me choice, a profile row
26
- * and the sign-in methods themselves, under `persist`. The other adds `isAnonymous` — and that
27
- * one is the reason this takes a `derive` function rather than just letting the caller spread
28
- * extra keys in: `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and
29
- * `clear`, which are exactly the two writes the product does not own.
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. What it says stays yours — "Copied", or "Select it
12
- * and copy it yourself", in a label or a toast — and so does announcing it: a label that changes
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. What it says stays yours — "Copied", or "Select it
22
- * and copy it yourself", in a label or a toast — and so does announcing it: a label that changes
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
- * 1. The next navigation asks for a chunk whose hashed name is gone. React's lazy boundary
9
- * throws, and the person gets a white screen on a click that worked a minute ago.
10
- * 2. The old bundle reads a field the API has since renamed, and throws. That one arrives as a
11
- * plain `TypeError` with nothing in it about a deploy — no file, no hash, no signal.
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
- * {@link isStaleBuild} asks the server, which is evidence rather than a guess — and it is the
16
- * only one that catches case 2 at all.
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 — `index.html`, at a 200, as
20
- * `text/html` — which is what the browser's "MIME type text/html" module-script refusal actually
21
- * is. Hosts then match cache rules against the REQUEST PATH rather than the outcome, so the
22
- * `immutable, max-age=31536000` rule written for hashed assets gets applied to that HTML body:
23
- * one poisoned asset URL, at the edge and in every browser that touched it, for a year. One
24
- * adopter removed the cause instead of recovering from it — no blanket fallback over `/assets/`,
25
- * and an edge function that refuses to answer an asset path with `text/html` — which turns a
26
- * dead chunk into a real, uncacheable 404 and lets everything below be recovery rather than the
27
- * plan. A package cannot ship that; it can refuse to let it go unwritten.
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 — recoverable by reloading.
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
- * here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
47
- * dynamically imported module", Safari "Importing a module script failed", webpack's
48
- * ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
49
- * which is the one that fires on a static host that has not fixed the cause above, because the
50
- * missing `.js` is answered with `index.html`.
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. Everything ambiguous —
60
- * offline, a non-200, a captive portal, a probe that times out — resolves to `false`: dressing a
61
- * genuine bug up as an update hides it from the user and from us.
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 — it would report "current"
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
- * per minute, so a genuinely broken deploy degrades to the error screen — which can at least say
78
- * something — instead of flickering forever.
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
- * When that link fails, the import itself still runs right after and usually succeeds — so this
91
- * one is worth swallowing, where a failure of the import is the real thing
92
- * {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
93
- * for a stylesheet link; script hints never reject.
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 — but taking that
100
- * deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
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
- * it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
104
- * runs. A real module failure is left to throw, where the error boundary can recognise it.
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
@@ -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
- * 1. The next navigation asks for a chunk whose hashed name is gone. React's lazy boundary
9
- * throws, and the person gets a white screen on a click that worked a minute ago.
10
- * 2. The old bundle reads a field the API has since renamed, and throws. That one arrives as a
11
- * plain `TypeError` with nothing in it about a deploy — no file, no hash, no signal.
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
- * {@link isStaleBuild} asks the server, which is evidence rather than a guess — and it is the
16
- * only one that catches case 2 at all.
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 — `index.html`, at a 200, as
20
- * `text/html` — which is what the browser's "MIME type text/html" module-script refusal actually
21
- * is. Hosts then match cache rules against the REQUEST PATH rather than the outcome, so the
22
- * `immutable, max-age=31536000` rule written for hashed assets gets applied to that HTML body:
23
- * one poisoned asset URL, at the edge and in every browser that touched it, for a year. One
24
- * adopter removed the cause instead of recovering from it — no blanket fallback over `/assets/`,
25
- * and an edge function that refuses to answer an asset path with `text/html` — which turns a
26
- * dead chunk into a real, uncacheable 404 and lets everything below be recovery rather than the
27
- * plan. A package cannot ship that; it can refuse to let it go unwritten.
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 — recoverable by reloading.
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
- * here: Chrome/Edge "Failed to fetch dynamically imported module", Firefox "error loading
51
- * dynamically imported module", Safari "Importing a module script failed", webpack's
52
- * ChunkLoadError and "Loading chunk N failed", and the SPA-fallback "MIME type" refusal —
53
- * which is the one that fires on a static host that has not fixed the cause above, because the
54
- * missing `.js` is answered with `index.html`.
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. Everything ambiguous —
90
- * offline, a non-200, a captive portal, a probe that times out — resolves to `false`: dressing a
91
- * genuine bug up as an update hides it from the user and from us.
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 — it would report "current"
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
- * per minute, so a genuinely broken deploy degrades to the error screen — which can at least say
124
- * something — instead of flickering forever.
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
- * When that link fails, the import itself still runs right after and usually succeeds — so this
150
- * one is worth swallowing, where a failure of the import is the real thing
151
- * {@link isChunkLoadError} matches. Vite builds this message in exactly one place and only ever
152
- * for a stylesheet link; script hints never reject.
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 — but taking that
161
- * deal unconditionally resolves the dynamic import with `undefined` (Vite's helper ends in
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
- * it, naming no chunk. So it is taken ONLY for a failed preload hint, where the import still
165
- * runs. A real module failure is left to throw, where the error boundary can recognise it.
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) => {