@dloizides/utils 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,101 +1,137 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [1.7.0] - 2026-08-03
9
-
10
- ### Added
11
-
12
- - `toCsv(rows, columns)` + `CsvColumn<T>` — an RFC 4180 CSV serializer (header row + one row per
13
- item; quotes fields containing `,` `"` CR/LF; `null`/`undefined` → empty cell; empty rows yield
14
- the header line alone). Pure and DOM-free — the escaping is the whole risk surface, so it lives
15
- here where a unit test can hammer it, split from any browser download shell (an app-owned,
16
- side-effecting concern). Exported from the root and as the `csv` namespace + `@dloizides/utils/csv`
17
- subpath. Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) on its 2nd-consumer horizon; the
18
- 9 RFC-4180 edge-case tests moved with it unchanged. The DOM `<a download>` shell that consumes it
19
- stays app-side until a 2nd consumer needs it.
20
-
21
- ## [1.4.0] - 2026-07-19
22
-
23
- ### Added
24
-
25
- - `sanitizeHtml(text)` / `removeControlCharacters(text)` / `sanitizeNotificationMessage(message, maxLength?)` /
26
- `sanitizeText(text, maxLength?)` / `sanitizeUrl(url)` — XSS defence-in-depth for user-provided
27
- text: HTML-entity escaping, control/zero-width/BOM stripping (homograph + hidden-content
28
- attacks), length capping, and `javascript:`/`data:`/`vbscript:` URL blocking. Pure string
29
- work with no imports at all, so it is safe on web and native alike. Also exported as the
30
- `sanitize` namespace. Promoted from the byte-identical `src/utils/sanitize.ts` pair in
31
- erevna-web and katalogos-web (de-fork Wave 1); all 35 tests moved with it unchanged.
32
-
33
- ⚠️ **Naming hazard, inherited — do not "fix" by guessing.** The `sanitizeHtml` exported here
34
- ESCAPES entities (`<` → `&lt;`) and returns display-safe text. `@dloizides/rn-web-hooks`
35
- exports a DIFFERENT `sanitizeHtml` — a DOMPurify scrubber that PARSES real DOM and strips
36
- dangerous nodes. The fork gave two different jobs one name. They are kept in separate
37
- packages so no single import site can resolve both, and neither was renamed here because
38
- this wave is a provably-safe move, not a redesign. A follow-up should rename the escaping
39
- one to `escapeHtml`.
40
-
41
- ## [1.3.0] - 2026-07-19
42
-
43
- ### Added
44
-
45
- - `isChunkLoadError(error)` / `attemptChunkRecovery(ports?)` / `clearChunkRecoveryFlag(ports?)` /
46
- `reloadPage(ports?)` + the `ChunkRecoveryPorts` type — stale-chunk detection and a one-shot
47
- guarded reload. After a deploy the browser can hold a stale `index.html` referencing hashed
48
- chunks that now 404; a single guarded `location.reload()` recovers silently, and the
49
- sessionStorage one-shot flag stops it becoming a reload LOOP when the reload does not help.
50
- Detection is a pure predicate; the storage + reload side effects are injectable ports.
51
- Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in kefi-web,
52
- erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd copy — the original file's
53
- own header predicted the move). Also exported as the `chunkRecovery` namespace.
54
- Pairs with `<AppErrorBoundary>` from `@dloizides/ui-feedback` ≥1.7.0, which consumes these as
55
- its `retryable` / `onMount` / `recover` injections.
56
-
57
- ## [1.2.0] - 2026-07-18
58
-
59
- ### Added
60
-
61
- - `setRedirectHandler(handler)` / `redirectTo(path)` / `resetRedirectHandler()` — a
62
- framework-agnostic redirect escape hatch for non-React callers (axios 401 interceptors,
63
- auth providers) that need to navigate without a router hook. Falls back through
64
- registered handler → `window.location.replace` → a queue drained when the router mounts,
65
- and forces a hard navigation if the router silently failed to change the URL.
66
- Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and
67
- katalogos-web (de-fork wave W1.1). Also exported as the `navigation` namespace.
68
-
69
- ### Fixed
70
-
71
- - The promoted watchdog guarded its `window` access with `try`/`catch` only; it now checks
72
- `typeof window !== 'undefined'` explicitly, so a non-browser host cannot reach a
73
- `ReferenceError` path at all. Surfaced by porting the code into a package whose tsconfig
74
- has no `DOM` lib.
75
-
76
- ## [1.1.0] - 2026-06-15
77
-
78
- ### Added
79
-
80
- - `getErrorMessage(value, fallback?)` — extract a human-readable message from any error shape
81
- (Error / string / `{message}`). Consolidates the duplicated app-side `errorMessage` helper.
82
- - `formatDate(date, locale, options?)` — locale-aware date formatting (pure; caller supplies the
83
- locale, e.g. from i18n). Backs the apps' `FD` helper. (Capability Wave C1, batch 6.)
84
-
85
- ## [1.0.0] - 2024-01-27
86
-
87
- ### Added
88
-
89
- - Initial release
90
- - Type guards:
91
- - `isValueDefined` - Check if value is not null or undefined
92
- - `isNotEmptyArray` - Check if value is a non-empty array
93
- - `isNotEmptyString` - Check if value is a non-empty string (after trim)
94
- - `isNullOrUndefined` - Check if value is null or undefined
95
- - `isEmptyArray` - Check if value is an empty array
96
- - `isEmptyString` - Check if value is empty or whitespace only
97
- - Assertions:
98
- - `assertDefined` - Assert that value is defined (throws if null/undefined)
99
- - Full TypeScript support with type narrowing
100
- - ESM and CommonJS module support
101
- - 100% test coverage
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `fireAndForget(promise)` (EXTRACT-TS-I18N-1 "Fold duplicated i18n and utils into @dloizides/i18n and @dloizides/utils"): starts a promise and swallows its rejection; replaces local copies in agora-web and ichnos-web.
13
+
14
+ ## [1.8.0] - 2026-09-09
15
+
16
+ ### Fixed
17
+
18
+ - `clearChunkRecoveryFlag` no longer releases the reload guard on a clean mount. The guard was a
19
+ sessionStorage boolean and `AppErrorBoundary` cleared it whenever the tree committed cleanly, so
20
+ on a lazy route after a rollout the boundary mounted clean, the flag was released, the lazy
21
+ import then 404'd after the commit, and nothing was left to stop the next reload — an unbounded
22
+ loop in kefi-web, erevna-web and katalogos-web. The flag is now a 60s cooldown timestamp
23
+ (`ui.chunkReload.attemptedAt`) and the clear refuses to release inside the window, so a route
24
+ that keeps failing reloads exactly once. `ChunkRecoveryPorts` changes shape accordingly.
25
+
26
+ ## [1.7.0] - 2026-08-03
27
+
28
+ ### Added
29
+
30
+ - `toCsv(rows, columns)` + `CsvColumn<T>` — an RFC 4180 CSV serializer (header row + one row per
31
+ item; quotes fields containing `,` `"` CR/LF; `null`/`undefined` → empty cell; empty rows yield
32
+ the header line alone). Pure and DOM-free — the escaping is the whole risk surface, so it lives
33
+ here where a unit test can hammer it, split from any browser download shell (an app-owned,
34
+ side-effecting concern). Exported from the root and as the `csv` namespace + `@dloizides/utils/csv`
35
+ subpath. Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) on its 2nd-consumer horizon; the
36
+ 9 RFC-4180 edge-case tests moved with it unchanged. The DOM `<a download>` shell that consumes it
37
+ stays app-side until a 2nd consumer needs it.
38
+
39
+ ## [1.4.0] - 2026-07-19
40
+
41
+ ### Added
42
+
43
+ - `sanitizeHtml(text)` / `removeControlCharacters(text)` / `sanitizeNotificationMessage(message, maxLength?)` /
44
+ `sanitizeText(text, maxLength?)` / `sanitizeUrl(url)` — XSS defence-in-depth for user-provided
45
+ text: HTML-entity escaping, control/zero-width/BOM stripping (homograph + hidden-content
46
+ attacks), length capping, and `javascript:`/`data:`/`vbscript:` URL blocking. Pure string
47
+ work with no imports at all, so it is safe on web and native alike. Also exported as the
48
+ `sanitize` namespace. Promoted from the byte-identical `src/utils/sanitize.ts` pair in
49
+ erevna-web and katalogos-web (de-fork Wave 1); all 35 tests moved with it unchanged.
50
+
51
+ ⚠️ **Naming hazard, inherited — do not "fix" by guessing.** The `sanitizeHtml` exported here
52
+ ESCAPES entities (`<` → `&lt;`) and returns display-safe text. `@dloizides/rn-web-hooks`
53
+ exports a DIFFERENT `sanitizeHtml` — a DOMPurify scrubber that PARSES real DOM and strips
54
+ dangerous nodes. The fork gave two different jobs one name. They are kept in separate
55
+ packages so no single import site can resolve both, and neither was renamed here because
56
+ this wave is a provably-safe move, not a redesign. A follow-up should rename the escaping
57
+ one to `escapeHtml`.
58
+
59
+ ## [1.3.0] - 2026-07-19
60
+
61
+ ### Added
62
+
63
+ - `isChunkLoadError(error)` / `attemptChunkRecovery(ports?)` / `clearChunkRecoveryFlag(ports?)` /
64
+ `reloadPage(ports?)` + the `ChunkRecoveryPorts` type — stale-chunk detection and a one-shot
65
+ guarded reload. After a deploy the browser can hold a stale `index.html` referencing hashed
66
+ chunks that now 404; a single guarded `location.reload()` recovers silently, and the
67
+ sessionStorage one-shot flag stops it becoming a reload LOOP when the reload does not help.
68
+ Detection is a pure predicate; the storage + reload side effects are injectable ports.
69
+ Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in kefi-web,
70
+ erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd copy — the original file's
71
+ own header predicted the move). Also exported as the `chunkRecovery` namespace.
72
+ Pairs with `<AppErrorBoundary>` from `@dloizides/ui-feedback` ≥1.7.0, which consumes these as
73
+ its `retryable` / `onMount` / `recover` injections.
74
+
75
+ ## [1.2.0] - 2026-07-18
76
+
77
+ ### Added
78
+
79
+ - `setRedirectHandler(handler)` / `redirectTo(path)` / `resetRedirectHandler()` — a
80
+ framework-agnostic redirect escape hatch for non-React callers (axios 401 interceptors,
81
+ auth providers) that need to navigate without a router hook. Falls back through
82
+ registered handler → `window.location.replace` → a queue drained when the router mounts,
83
+ and forces a hard navigation if the router silently failed to change the URL.
84
+ Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and
85
+ katalogos-web (de-fork wave W1.1). Also exported as the `navigation` namespace.
86
+
87
+ ### Fixed
88
+
89
+ - The promoted watchdog guarded its `window` access with `try`/`catch` only; it now checks
90
+ `typeof window !== 'undefined'` explicitly, so a non-browser host cannot reach a
91
+ `ReferenceError` path at all. Surfaced by porting the code into a package whose tsconfig
92
+ has no `DOM` lib.
93
+
94
+ ## [1.1.0] - 2026-06-15
95
+
96
+ ### Added
97
+
98
+ - `getErrorMessage(value, fallback?)` — extract a human-readable message from any error shape
99
+ (Error / string / `{message}`). Consolidates the duplicated app-side `errorMessage` helper.
100
+ - `formatDate(date, locale, options?)` — locale-aware date formatting (pure; caller supplies the
101
+ locale, e.g. from i18n). Backs the apps' `FD` helper. (Capability Wave C1, batch 6.)
102
+
103
+ ## [1.0.0] - 2024-01-27
104
+
105
+ ### Added
106
+
107
+ - Initial release
108
+ - Type guards:
109
+ - `isValueDefined` - Check if value is not null or undefined
110
+ - `isNotEmptyArray` - Check if value is a non-empty array
111
+ - `isNotEmptyString` - Check if value is a non-empty string (after trim)
112
+ - `isNullOrUndefined` - Check if value is null or undefined
113
+ - `isEmptyArray` - Check if value is an empty array
114
+ - `isEmptyString` - Check if value is empty or whitespace only
115
+ - Assertions:
116
+ - `assertDefined` - Assert that value is defined (throws if null/undefined)
117
+ - Full TypeScript support with type narrowing
118
+ - ESM and CommonJS module support
119
+ - 100% test coverage
120
+
121
+ ## 1.8.0
122
+
123
+ ### chunkRecovery — the reload guard is now a COOLDOWN, not a boolean released on mount
124
+
125
+ The one-shot boolean `ui.chunkReload.attempted` was released by consumers from the root
126
+ boundary's clean-mount hook. On a lazy route that mount is clean — a Suspense fallback
127
+ renders, `componentDidMount` fires, and only THEN does the dynamic import reject with a
128
+ 404 chunk. The release re-armed recovery on every pass, so the reload ran unbounded.
129
+
130
+ The guard is now the epoch-ms timestamp `ui.chunkReload.attemptedAt`, and a recorded
131
+ attempt blocks another automatic reload for 60s. `clearChunkRecoveryFlag` refuses to clear
132
+ a record still inside that window, so the bound holds no matter when a consumer calls it.
133
+ A genuinely later rollout still auto-recovers once, because the window expires by itself.
134
+
135
+ BREAKING for anyone injecting `ChunkRecoveryPorts` (test code only — every app uses the
136
+ window defaults, so app call sites are unchanged): `hasFlag`/`setFlag`/`clearFlag` are now
137
+ `readAttemptAt`/`recordAttemptAt`/`clearAttempt`, plus a `now` port.
package/dist/index.d.mts CHANGED
@@ -2,6 +2,9 @@ export { i as guards, a as isEmptyArray, b as isEmptyString, c as isNotEmptyArra
2
2
  export { a as assertDefined, i as assertions } from './index-DmagV9KM.mjs';
3
3
  export { C as CsvColumn, i as csv, t as toCsv } from './index-BYi0aqOQ.mjs';
4
4
 
5
+ /** Starts a promise without awaiting it and swallows its rejection. */
6
+ declare function fireAndForget(promise: Promise<unknown>): void;
7
+
5
8
  /**
6
9
  * Extracts a human-readable error message from various error shapes
7
10
  * (Error, string, or an object with a `message` property).
@@ -144,24 +147,41 @@ declare function resetRedirectHandler(): void;
144
147
  */
145
148
  /** Injectable side-effect ports; default to the real `window`. */
146
149
  interface ChunkRecoveryPorts {
147
- hasFlag: () => boolean;
148
- setFlag: () => void;
149
- clearFlag: () => void;
150
+ /** Epoch-ms of the last recorded attempt, or `null` when none is stored. */
151
+ readAttemptAt: () => number | null;
152
+ recordAttemptAt: (at: number) => void;
153
+ clearAttempt: () => void;
150
154
  reload: () => void;
155
+ now: () => number;
151
156
  }
152
157
  /** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */
153
158
  declare function isChunkLoadError(error: unknown): boolean;
154
159
  /**
155
- * Attempt a one-shot reload to recover from a stale-chunk error.
156
- * Returns `true` when a reload was triggered, `false` when the one-shot guard was
157
- * already spent (the caller then shows a manual Reload action, never a loop).
160
+ * Attempt a guarded reload to recover from a stale-chunk error.
161
+ * Returns `true` when a reload was triggered, `false` while the cooldown from the previous
162
+ * attempt is still running (the caller then shows a manual Reload action, never a loop).
163
+ *
164
+ * `recordAttemptAt` and `reload` stay synchronous and adjacent so the record survives the
165
+ * navigation that follows.
158
166
  */
159
167
  declare function attemptChunkRecovery(ports?: ChunkRecoveryPorts): boolean;
160
- /** Clear the one-shot guard after a clean load so a FUTURE deploy can auto-recover. */
168
+ /**
169
+ * Release the guard after a clean load — but ONLY once the cooldown has expired.
170
+ *
171
+ * Consumers call this from the root boundary's clean-mount hook, and on a lazy route that
172
+ * mount happens BEFORE the chunk rejects. Refusing to clear a live record is what makes the
173
+ * bound hold no matter when the consumer calls this: an expired record permits recovery
174
+ * anyway, so clearing it only tidies storage, while a live one is left alone.
175
+ */
161
176
  declare function clearChunkRecoveryFlag(ports?: ChunkRecoveryPorts): void;
162
177
  /** Manually reload the page (the Reload action). */
163
178
  declare function reloadPage(ports?: ChunkRecoveryPorts): void;
164
179
 
180
+ declare const index$5_fireAndForget: typeof fireAndForget;
181
+ declare namespace index$5 {
182
+ export { index$5_fireAndForget as fireAndForget };
183
+ }
184
+
165
185
  declare const index$4_getErrorMessage: typeof getErrorMessage;
166
186
  declare namespace index$4 {
167
187
  export { index$4_getErrorMessage as getErrorMessage };
@@ -197,4 +217,4 @@ declare namespace index {
197
217
  export { index_removeControlCharacters as removeControlCharacters, index_sanitizeHtml as sanitizeHtml, index_sanitizeNotificationMessage as sanitizeNotificationMessage, index_sanitizeText as sanitizeText, index_sanitizeUrl as sanitizeUrl };
198
218
  }
199
219
 
200
- export { type ChunkRecoveryPorts, attemptChunkRecovery, index$1 as chunkRecovery, clearChunkRecoveryFlag, index$4 as error, index$3 as format, formatDate, getErrorMessage, isChunkLoadError, index$2 as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, index as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler };
220
+ export { type ChunkRecoveryPorts, index$5 as async, attemptChunkRecovery, index$1 as chunkRecovery, clearChunkRecoveryFlag, index$4 as error, fireAndForget, index$3 as format, formatDate, getErrorMessage, isChunkLoadError, index$2 as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, index as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export { i as guards, a as isEmptyArray, b as isEmptyString, c as isNotEmptyArra
2
2
  export { a as assertDefined, i as assertions } from './index-DmagV9KM.js';
3
3
  export { C as CsvColumn, i as csv, t as toCsv } from './index-BYi0aqOQ.js';
4
4
 
5
+ /** Starts a promise without awaiting it and swallows its rejection. */
6
+ declare function fireAndForget(promise: Promise<unknown>): void;
7
+
5
8
  /**
6
9
  * Extracts a human-readable error message from various error shapes
7
10
  * (Error, string, or an object with a `message` property).
@@ -144,24 +147,41 @@ declare function resetRedirectHandler(): void;
144
147
  */
145
148
  /** Injectable side-effect ports; default to the real `window`. */
146
149
  interface ChunkRecoveryPorts {
147
- hasFlag: () => boolean;
148
- setFlag: () => void;
149
- clearFlag: () => void;
150
+ /** Epoch-ms of the last recorded attempt, or `null` when none is stored. */
151
+ readAttemptAt: () => number | null;
152
+ recordAttemptAt: (at: number) => void;
153
+ clearAttempt: () => void;
150
154
  reload: () => void;
155
+ now: () => number;
151
156
  }
152
157
  /** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */
153
158
  declare function isChunkLoadError(error: unknown): boolean;
154
159
  /**
155
- * Attempt a one-shot reload to recover from a stale-chunk error.
156
- * Returns `true` when a reload was triggered, `false` when the one-shot guard was
157
- * already spent (the caller then shows a manual Reload action, never a loop).
160
+ * Attempt a guarded reload to recover from a stale-chunk error.
161
+ * Returns `true` when a reload was triggered, `false` while the cooldown from the previous
162
+ * attempt is still running (the caller then shows a manual Reload action, never a loop).
163
+ *
164
+ * `recordAttemptAt` and `reload` stay synchronous and adjacent so the record survives the
165
+ * navigation that follows.
158
166
  */
159
167
  declare function attemptChunkRecovery(ports?: ChunkRecoveryPorts): boolean;
160
- /** Clear the one-shot guard after a clean load so a FUTURE deploy can auto-recover. */
168
+ /**
169
+ * Release the guard after a clean load — but ONLY once the cooldown has expired.
170
+ *
171
+ * Consumers call this from the root boundary's clean-mount hook, and on a lazy route that
172
+ * mount happens BEFORE the chunk rejects. Refusing to clear a live record is what makes the
173
+ * bound hold no matter when the consumer calls this: an expired record permits recovery
174
+ * anyway, so clearing it only tidies storage, while a live one is left alone.
175
+ */
161
176
  declare function clearChunkRecoveryFlag(ports?: ChunkRecoveryPorts): void;
162
177
  /** Manually reload the page (the Reload action). */
163
178
  declare function reloadPage(ports?: ChunkRecoveryPorts): void;
164
179
 
180
+ declare const index$5_fireAndForget: typeof fireAndForget;
181
+ declare namespace index$5 {
182
+ export { index$5_fireAndForget as fireAndForget };
183
+ }
184
+
165
185
  declare const index$4_getErrorMessage: typeof getErrorMessage;
166
186
  declare namespace index$4 {
167
187
  export { index$4_getErrorMessage as getErrorMessage };
@@ -197,4 +217,4 @@ declare namespace index {
197
217
  export { index_removeControlCharacters as removeControlCharacters, index_sanitizeHtml as sanitizeHtml, index_sanitizeNotificationMessage as sanitizeNotificationMessage, index_sanitizeText as sanitizeText, index_sanitizeUrl as sanitizeUrl };
198
218
  }
199
219
 
200
- export { type ChunkRecoveryPorts, attemptChunkRecovery, index$1 as chunkRecovery, clearChunkRecoveryFlag, index$4 as error, index$3 as format, formatDate, getErrorMessage, isChunkLoadError, index$2 as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, index as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler };
220
+ export { type ChunkRecoveryPorts, index$5 as async, attemptChunkRecovery, index$1 as chunkRecovery, clearChunkRecoveryFlag, index$4 as error, fireAndForget, index$3 as format, formatDate, getErrorMessage, isChunkLoadError, index$2 as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, index as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler };
package/dist/index.js CHANGED
@@ -43,6 +43,11 @@ function assertDefined(value, message) {
43
43
  }
44
44
  }
45
45
 
46
+ // src/async/fireAndForget.ts
47
+ function fireAndForget(promise) {
48
+ promise.catch(() => void 0);
49
+ }
50
+
46
51
  // src/error/getErrorMessage.ts
47
52
  function isErrorWithMessage(value) {
48
53
  return typeof value === "object" && isValueDefined(value) && "message" in value;
@@ -258,7 +263,8 @@ function resetRedirectHandler() {
258
263
  }
259
264
 
260
265
  // src/chunkRecovery/chunkRecovery.ts
261
- var CHUNK_RELOAD_FLAG = "ui.chunkReload.attempted";
266
+ var CHUNK_RELOAD_AT = "ui.chunkReload.attemptedAt";
267
+ var RECOVERY_COOLDOWN_MS = 6e4;
262
268
  var CHUNK_ERROR_PATTERNS = [
263
269
  /ChunkLoadError/i,
264
270
  /Loading chunk [\w-]+ failed/i,
@@ -292,13 +298,21 @@ function defaultPorts() {
292
298
  const storage = resolveStorage();
293
299
  const hasWindow = typeof window !== "undefined";
294
300
  return {
295
- hasFlag: () => Boolean(storage?.getItem(CHUNK_RELOAD_FLAG)),
296
- setFlag: () => {
297
- storage?.setItem(CHUNK_RELOAD_FLAG, "1");
301
+ readAttemptAt: () => {
302
+ const raw = storage?.getItem(CHUNK_RELOAD_AT);
303
+ if (raw === null || raw === void 0 || raw === "") {
304
+ return null;
305
+ }
306
+ const parsed = Number(raw);
307
+ return Number.isFinite(parsed) ? parsed : null;
298
308
  },
299
- clearFlag: () => {
300
- storage?.removeItem(CHUNK_RELOAD_FLAG);
309
+ recordAttemptAt: (at) => {
310
+ storage?.setItem(CHUNK_RELOAD_AT, String(at));
301
311
  },
312
+ clearAttempt: () => {
313
+ storage?.removeItem(CHUNK_RELOAD_AT);
314
+ },
315
+ now: () => Date.now(),
302
316
  reload: () => {
303
317
  if (hasWindow) {
304
318
  window.location.reload();
@@ -306,16 +320,23 @@ function defaultPorts() {
306
320
  }
307
321
  };
308
322
  }
323
+ function isWithinCooldown(attemptedAt, now) {
324
+ return attemptedAt !== null && now - attemptedAt < RECOVERY_COOLDOWN_MS;
325
+ }
309
326
  function attemptChunkRecovery(ports = defaultPorts()) {
310
- if (ports.hasFlag()) {
327
+ const now = ports.now();
328
+ if (isWithinCooldown(ports.readAttemptAt(), now)) {
311
329
  return false;
312
330
  }
313
- ports.setFlag();
331
+ ports.recordAttemptAt(now);
314
332
  ports.reload();
315
333
  return true;
316
334
  }
317
335
  function clearChunkRecoveryFlag(ports = defaultPorts()) {
318
- ports.clearFlag();
336
+ if (isWithinCooldown(ports.readAttemptAt(), ports.now())) {
337
+ return;
338
+ }
339
+ ports.clearAttempt();
319
340
  }
320
341
  function reloadPage(ports = defaultPorts()) {
321
342
  ports.reload();
@@ -338,6 +359,12 @@ __export(assertions_exports, {
338
359
  assertDefined: () => assertDefined
339
360
  });
340
361
 
362
+ // src/async/index.ts
363
+ var async_exports = {};
364
+ __export(async_exports, {
365
+ fireAndForget: () => fireAndForget
366
+ });
367
+
341
368
  // src/error/index.ts
342
369
  var error_exports = {};
343
370
  __export(error_exports, {
@@ -385,11 +412,13 @@ __export(sanitize_exports, {
385
412
 
386
413
  exports.assertDefined = assertDefined;
387
414
  exports.assertions = assertions_exports;
415
+ exports.async = async_exports;
388
416
  exports.attemptChunkRecovery = attemptChunkRecovery;
389
417
  exports.chunkRecovery = chunkRecovery_exports;
390
418
  exports.clearChunkRecoveryFlag = clearChunkRecoveryFlag;
391
419
  exports.csv = csv_exports;
392
420
  exports.error = error_exports;
421
+ exports.fireAndForget = fireAndForget;
393
422
  exports.format = format_exports;
394
423
  exports.formatDate = formatDate;
395
424
  exports.getErrorMessage = getErrorMessage;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/guards/isValueDefined.ts","../src/guards/isNotEmptyArray.ts","../src/guards/isNotEmptyString.ts","../src/guards/isNullOrUndefined.ts","../src/guards/isEmptyArray.ts","../src/guards/isEmptyString.ts","../src/assertions/assertDefined.ts","../src/error/getErrorMessage.ts","../src/format/formatDate.ts","../src/csv/toCsv.ts","../src/sanitize/sanitize.ts","../src/navigation/redirect.ts","../src/chunkRecovery/chunkRecovery.ts","../src/guards/index.ts","../src/assertions/index.ts","../src/error/index.ts","../src/format/index.ts","../src/csv/index.ts","../src/navigation/index.ts","../src/chunkRecovery/index.ts","../src/sanitize/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * Type guard that checks if a value is neither null nor undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is defined (not null and not undefined)\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isValueDefined(value)) {\r\n * // value is narrowed to string\r\n * console.log(value.length);\r\n * }\r\n * ```\r\n */\r\nexport function isValueDefined<T>(value: T | null | undefined): value is T {\r\n return value !== undefined && value !== null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is a non-empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with at least one element\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] | undefined = getItems();\r\n * if (isNotEmptyArray(items)) {\r\n * // items is narrowed to string[] with at least one element\r\n * console.log(items[0]); // Safe to access\r\n * }\r\n * ```\r\n */\r\nexport function isNotEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: number } {\r\n return Array.isArray(value) && value.length > 0;\r\n}\r\n","/**\r\n * Checks if a value is a non-empty string (after trimming whitespace).\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is a string with non-whitespace content\r\n *\r\n * @example\r\n * ```typescript\r\n * isNotEmptyString('hello'); // true\r\n * isNotEmptyString(' '); // false\r\n * isNotEmptyString(''); // false\r\n * isNotEmptyString(undefined); // false\r\n * ```\r\n */\r\nexport function isNotEmptyString(value: string | null | undefined): value is string {\r\n return typeof value === 'string' && value.trim().length > 0;\r\n}\r\n","/**\r\n * Type guard that checks if a value is null or undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isNullOrUndefined(value)) {\r\n * // value is narrowed to null | undefined\r\n * console.log('Value is not set');\r\n * }\r\n * ```\r\n */\r\nexport function isNullOrUndefined(value: unknown): value is null | undefined {\r\n return value === undefined || value === null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is an empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with zero elements\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] = getItems();\r\n * if (isEmptyArray(items)) {\r\n * console.log('No items found');\r\n * }\r\n * ```\r\n */\r\nexport function isEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: 0 } {\r\n return Array.isArray(value) && value.length === 0;\r\n}\r\n","/**\r\n * Checks if a value is an empty string or contains only whitespace.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an empty string or whitespace-only\r\n *\r\n * @example\r\n * ```typescript\r\n * isEmptyString(''); // true\r\n * isEmptyString(' '); // true\r\n * isEmptyString('hello'); // false\r\n * isEmptyString(undefined); // false (not a string)\r\n * isEmptyString(null); // false (not a string)\r\n * ```\r\n */\r\nexport function isEmptyString(value: string | null | undefined): boolean {\r\n return typeof value === 'string' && value.trim().length === 0;\r\n}\r\n","/**\r\n * Asserts that a value is defined (not null or undefined).\r\n * Throws an error if the value is null or undefined.\r\n *\r\n * @param value - The value to assert\r\n * @param message - Optional error message\r\n * @throws Error if value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = getUser(); // User | null\r\n * assertDefined(user, 'User must exist');\r\n * // user is now narrowed to User\r\n * console.log(user.name);\r\n * ```\r\n */\r\nexport function assertDefined<T>(\r\n value: T | null | undefined,\r\n message?: string\r\n): asserts value is T {\r\n if (value === undefined || value === null) {\r\n throw new Error(message ?? 'Value must be defined');\r\n }\r\n}\r\n","import { isValueDefined } from '../guards/isValueDefined';\n\ninterface ErrorWithMessage {\n message?: unknown;\n}\n\nfunction isErrorWithMessage(value: unknown): value is ErrorWithMessage {\n return typeof value === 'object' && isValueDefined(value) && 'message' in value;\n}\n\n/**\n * Extracts a human-readable error message from various error shapes\n * (Error, string, or an object with a `message` property).\n *\n * @param value - The error value.\n * @param fallback - Returned when no message can be extracted (default: 'Unknown error').\n */\nexport function getErrorMessage(value: unknown, fallback = 'Unknown error'): string {\n if (value instanceof Error) {return value.message;}\n if (typeof value === 'string') {return value;}\n if (isErrorWithMessage(value)) {\n const message = value.message;\n if (typeof message === 'string' && message.length > 0) {return message;}\n }\n return fallback;\n}\n","/**\n * Locale-aware date formatting (pure — the caller supplies the locale, e.g. from i18n).\n * Returns '' for null/undefined; falls back to the ISO date if Intl formatting throws.\n *\n * @example formatDate(new Date(), 'en') // \"6/15/2024\"\n * @example formatDate(new Date(), 'en', { year: 'numeric', month: 'long' }) // \"June 2024\"\n * @example formatDate(null, 'en') // \"\"\n */\nexport function formatDate(\n date: Date | null | undefined,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n if (date === null || date === undefined) {return '';}\n\n const resolvedLocale = locale !== '' ? locale : 'en';\n\n try {\n return new Intl.DateTimeFormat(resolvedLocale, options).format(date);\n } catch {\n // Fallback to ISO date if formatting fails (invalid locale/options).\n return date.toISOString().split('T')[0] ?? '';\n }\n}\n","/**\n * RFC 4180 CSV serialization — the pure, DOM-free core of \"export the rows on screen to CSV\".\n *\n * The escaping is the whole risk surface: a comma, a quote or a newline inside a value (a\n * counterparty name, an address) is exactly what turns one row into two columns or two rows in a\n * spreadsheet. So the serializer lives here as a pure, total function a unit test can hammer, split\n * from any browser download shell (which is a side-effecting, app-owned concern).\n *\n * Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) — the escaping logic is universal across\n * the fleet; the DOM `<a download>` shell that consumes it stays in the app until a 2nd consumer.\n */\nimport { isValueDefined } from '../guards/isValueDefined';\n\n/** One column of a CSV export: a (caller-localized) header and how to read the cell from a row. */\nexport interface CsvColumn<T> {\n /** The column header — already localized by the caller. */\n header: string;\n /** Reads the cell value from a row. `null`/`undefined` become an empty cell. */\n value: (row: T) => string | number | null | undefined;\n}\n\n/** RFC 4180: a field must be quoted when it contains a quote, a comma, or a line break. */\nconst NEEDS_QUOTING = /[\"',\\r\\n]/;\n\n// A more permissive test than strictly required (it also quotes apostrophes), so a value like\n// O'Brien is never split by a spreadsheet that treats a leading quote specially — harmless, and it\n// keeps the rule to one regex.\nconst QUOTE = '\"';\nconst ESCAPED_QUOTE = '\"\"';\n/** Every embedded quote, so each is doubled per RFC 4180 (ES2020-safe; no `replaceAll`). */\nconst EVERY_QUOTE = /\"/g;\n\n/** RFC 4180 uses CRLF between records; Excel and Sheets both require it. */\nconst ROW_SEPARATOR = '\\r\\n';\nconst FIELD_SEPARATOR = ',';\n\n/** Quote and escape a single field per RFC 4180. `null`/`undefined` → an empty field. */\nfunction escapeField(raw: string | number | null | undefined): string {\n const text = isValueDefined(raw) ? String(raw) : '';\n if (!NEEDS_QUOTING.test(text)) {\n return text;\n }\n return `${QUOTE}${text.replace(EVERY_QUOTE, ESCAPED_QUOTE)}${QUOTE}`;\n}\n\n/**\n * Serialize rows to an RFC 4180 CSV string — a header row followed by one row per item.\n *\n * Pure and total: an empty `rows` yields the header line alone, which opens as an empty-but-labelled\n * spreadsheet rather than a zero-byte file the operator cannot tell apart from a failure.\n */\nexport function toCsv<T>(rows: readonly T[], columns: ReadonlyArray<CsvColumn<T>>): string {\n const headerLine = columns.map((column) => escapeField(column.header)).join(FIELD_SEPARATOR);\n\n const dataLines = rows.map((row) =>\n columns.map((column) => escapeField(column.value(row))).join(FIELD_SEPARATOR),\n );\n\n return [headerLine, ...dataLines].join(ROW_SEPARATOR);\n}\n","/**\n * Text sanitization utilities for preventing XSS and ensuring safe display.\n * These utilities sanitize user-provided text before rendering.\n */\n\n/**\n * HTML entities that should be escaped to prevent XSS attacks.\n */\nconst HTML_ENTITIES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#x27;',\n '/': '&#x2F;',\n '`': '&#x60;',\n '=': '&#x3D;',\n};\n\n/**\n * Regular expression matching characters that need escaping.\n */\nconst HTML_ESCAPE_REGEX = /[&<>\"'`=/]/g;\n\n/**\n * Unicode code point ranges for dangerous characters.\n * These are control characters and zero-width characters that can be\n * used in homograph attacks or to hide malicious content.\n */\n\n/** C0 control characters range start (NULL) */\nconst C0_CONTROL_START = 0x0000;\n/** C0 control characters range end (Unit Separator) */\nconst C0_CONTROL_END = 0x001f;\n/** C1 control characters range start (DEL and beyond) */\nconst C1_CONTROL_START = 0x007f;\n/** C1 control characters range end */\nconst C1_CONTROL_END = 0x009f;\n/** Zero-width characters range start (Zero Width Space) */\nconst ZERO_WIDTH_START = 0x200b;\n/** Zero-width characters range end (Right-to-Left Mark) */\nconst ZERO_WIDTH_END = 0x200f;\n/** Format characters range start (Line Separator) */\nconst FORMAT_CHARS_START = 0x2028;\n/** Format characters range end (Narrow No-Break Space) */\nconst FORMAT_CHARS_END = 0x202f;\n/** Byte Order Mark code point */\nconst BYTE_ORDER_MARK = 0xfeff;\n\n/**\n * Maximum reasonable length for notification messages.\n */\nconst MAX_NOTIFICATION_LENGTH = 500;\n\n/**\n * Length of ellipsis suffix (\"...\") used when truncating text.\n */\nconst ELLIPSIS_LENGTH = 3;\n\n/**\n * Escapes HTML special characters in a string to prevent XSS attacks.\n * Use this when displaying user-provided content in the UI.\n *\n * @param text - The text to sanitize\n * @returns The sanitized text with HTML entities escaped\n *\n * @example\n * sanitizeHtml('<script>alert(\"xss\")</script>')\n * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'\n */\nexport function sanitizeHtml(text: string): string {\n if (text.length === 0) {return '';}\n return text.replace(HTML_ESCAPE_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Control characters and zero-width characters that should be removed.\n * These can be used in homograph attacks or to hide malicious content.\n */\nfunction isDangerousCharCode(code: number): boolean {\n const isC0Control = code >= C0_CONTROL_START && code <= C0_CONTROL_END;\n const isC1Control = code >= C1_CONTROL_START && code <= C1_CONTROL_END;\n const isZeroWidth = code >= ZERO_WIDTH_START && code <= ZERO_WIDTH_END;\n const isFormatChar = code >= FORMAT_CHARS_START && code <= FORMAT_CHARS_END;\n const isBom = code === BYTE_ORDER_MARK;\n return isC0Control || isC1Control || isZeroWidth || isFormatChar || isBom;\n}\n\n/**\n * Removes control characters and zero-width characters from text.\n * These characters can be used to create misleading text or hide content.\n *\n * @param text - The text to sanitize\n * @returns The text with dangerous characters removed\n */\nexport function removeControlCharacters(text: string): string {\n if (text.length === 0) {return '';}\n let out = '';\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (!isDangerousCharCode(code)) {out += text[i];}\n }\n return out;\n}\n\n/**\n * Sanitizes a notification message for safe display.\n * This function:\n * 1. Removes control/zero-width characters\n * 2. Trims whitespace\n * 3. Truncates to a reasonable length\n * 4. Escapes HTML entities (for web platform)\n *\n * Note: React Native's Text component already escapes HTML by default,\n * but this provides defense-in-depth and handles the web platform.\n *\n * @param message - The notification message to sanitize\n * @param maxLength - Maximum length (default: 500)\n * @returns The sanitized message\n *\n * @example\n * sanitizeNotificationMessage(' Hello <b>World</b> ')\n * // Returns: 'Hello &lt;b&gt;World&lt;/b&gt;'\n */\nexport function sanitizeNotificationMessage(\n message: string,\n maxLength: number = MAX_NOTIFICATION_LENGTH\n): string {\n if (message.length === 0) {return '';}\n\n // Remove dangerous characters first\n let sanitized = removeControlCharacters(message);\n\n // Trim whitespace\n sanitized = sanitized.trim();\n\n // Truncate if too long\n if (sanitized.length > maxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n // Escape HTML entities\n sanitized = sanitizeHtml(sanitized);\n\n return sanitized;\n}\n\n/**\n * Sanitizes text for safe display, without HTML escaping.\n * Use this for React Native components where HTML escaping isn't needed\n * but control character removal and length limits are still important.\n *\n * @param text - The text to sanitize\n * @param maxLength - Maximum length (optional)\n * @returns The sanitized text\n */\nexport function sanitizeText(text: string, maxLength?: number): string {\n if (text.length === 0) {return '';}\n\n let sanitized = removeControlCharacters(text);\n sanitized = sanitized.trim();\n\n const hasValidMaxLength = typeof maxLength === 'number' && maxLength > 0;\n const exceedsMaxLength = hasValidMaxLength && sanitized.length > maxLength;\n if (exceedsMaxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n return sanitized;\n}\n\n/**\n * Validates and sanitizes a URL to prevent javascript: and data: protocol attacks.\n *\n * @param url - The URL to validate\n * @returns The sanitized URL or empty string if invalid\n */\nexport function sanitizeUrl(url: string): string {\n if (url.length === 0) {return '';}\n\n const trimmedLower = url.trim().toLowerCase();\n const protocol = trimmedLower.split(':')[0];\n\n // Block dangerous protocols\n const isDangerousProtocol = protocol === 'javascript' || protocol === 'data' || protocol === 'vbscript';\n if (isDangerousProtocol) \n {return '';}\n \n\n return url.trim();\n}\n","/**\n * Framework-agnostic redirect escape hatch.\n *\n * WHY THIS EXISTS: non-React code (an axios 401 interceptor, an auth provider's\n * token-refresh failure path) needs to navigate, but has no access to a router\n * hook. The app registers its router's push/replace once via `setRedirectHandler`;\n * everything else calls `redirectTo` and does not care which router is mounted.\n *\n * Three layers of fallback, in order:\n * 1. the registered handler (the app's router),\n * 2. `window.location.replace` if no handler is registered yet,\n * 3. a queue, drained on the next `setRedirectHandler` call — so a redirect fired\n * before the router mounts is not silently dropped.\n *\n * Layer 1 additionally schedules a watchdog: some routers accept a `push` for a\n * route they cannot resolve and leave the URL untouched, which strands the user on\n * the page they were supposed to leave. If the location has not changed after\n * `NAVIGATION_CHECK_DELAY_MS`, a hard `window.location.replace` is forced.\n *\n * Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and\n * katalogos-web. Pure DOM + timers — no React, no router, no framework import.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020`. Adding DOM globally would let every\n * other util in the package reach for browser globals that are absent at runtime\n * on the server. Declaring the ~4 properties used here keeps that door shut.\n */\ninterface BrowserWindow {\n location: {\n pathname: string;\n search: string;\n hash: string;\n replace: (url: string) => void;\n };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** Delay in ms before checking whether the registered handler actually navigated. */\nconst NAVIGATION_CHECK_DELAY_MS = 150;\n\nlet redirectHandler: ((path: string) => void) | null = null;\nconst queuedRedirects: string[] = [];\n\n/**\n * Strips Expo-Router group segments — `/(protected)/settings` → `/settings`.\n * Group folders are a routing-config device and never appear in a real URL, so a\n * raw group path handed to `window.location` would 404.\n */\nfunction normalizeRouterPath(path: string): string {\n const normalized = path.replace(/\\/\\([^/]+\\)/g, '');\n return normalized === '' ? '/' : normalized;\n}\n\n/** Current location as path+search+hash, or null outside a browser (SSR/native). */\nfunction getCurrentLocation(): string | null {\n if (typeof window === 'undefined') {return null;}\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\n/** Force a hard navigation if the registered handler left the location unchanged. */\nfunction scheduleFallbackNavigation(before: string, normalized: string): void {\n setTimeout(() => {\n try {\n const after = getCurrentLocation();\n if (after === before && typeof window !== 'undefined') {window.location.replace(normalized);}\n } catch {\n /* ignore — window may have gone away (unmount/teardown) */\n }\n }, NAVIGATION_CHECK_DELAY_MS);\n}\n\n/** Attempt a hard browser navigation. Returns false outside a browser or on throw. */\nfunction tryWindowNavigation(normalized: string): boolean {\n if (typeof window === 'undefined') {return false;}\n try {\n window.location.replace(normalized);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Register the app's router navigate function and drain any redirects that were\n * queued before it mounted. Call once, from the root layout.\n */\nexport function setRedirectHandler(h: (path: string) => void): void {\n redirectHandler = h;\n while (queuedRedirects.length > 0) {\n const p = queuedRedirects.shift();\n try {\n if (typeof p === 'string' && p.length > 0) {h(normalizeRouterPath(p));}\n } catch {\n /* swallow; the handler may still not be ready */\n }\n }\n}\n\n/**\n * Navigate to `path` from anywhere, including outside React.\n * Safe to call before the router mounts — the redirect is queued and replayed.\n */\nexport function redirectTo(path: string): void {\n const normalized = normalizeRouterPath(path);\n if (redirectHandler) {\n const before = getCurrentLocation();\n try {\n redirectHandler(normalized);\n if (typeof before === 'string') {scheduleFallbackNavigation(before, normalized);}\n return;\n } catch {\n /* fall through to a hard navigation */\n }\n }\n if (tryWindowNavigation(normalized)) {return;}\n queuedRedirects.push(normalized);\n}\n\n/**\n * Clear the registered handler and any queued redirects.\n * Exists for tests — module state otherwise leaks between cases.\n */\nexport function resetRedirectHandler(): void {\n redirectHandler = null;\n queuedRedirects.length = 0;\n}\n","/**\n * Detection + one-shot auto-recovery for stale-chunk load failures.\n *\n * WHY (UX Move 3, would have masked P0-01): after a deploy, the browser may hold\n * a stale `index.html` that references hashed JS chunks which now 404 — surfacing\n * as a `ChunkLoadError` / \"failed to fetch dynamically imported module\". A single\n * guarded `location.reload()` re-fetches the fresh entry + chunks and the app\n * recovers with no user-visible error. The sessionStorage one-shot flag prevents\n * a reload LOOP if the reload does not fix it (then the boundary shows a manual\n * Reload action instead).\n *\n * Pure + port-injectable: detection is a pure predicate; the storage + reload\n * side effects are ports (default to `window`) so unit tests run deterministically\n * in jsdom without a real navigation.\n *\n * Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in\n * kefi-web, erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd).\n * The original file's own header predicted this move. Pairs with\n * `<AppErrorBoundary>` from `@dloizides/ui-feedback`, which consumes these as its\n * `retryable` / `onMount` / `recover` injections.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020` (see `navigation/redirect.ts`, which\n * makes the same call). Declaring the few properties used here keeps every other\n * util in the package from reaching for browser globals absent on the server.\n */\ninterface ChunkRecoveryStorage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n removeItem: (key: string) => void;\n}\n\ninterface BrowserWindow {\n sessionStorage: ChunkRecoveryStorage;\n location: { reload: () => void };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** sessionStorage key for the one-shot auto-reload guard. */\nconst CHUNK_RELOAD_FLAG = 'ui.chunkReload.attempted';\n\n/** Messages/names emitted by webpack/metro/vite/Safari for a stale-chunk failure. */\nconst CHUNK_ERROR_PATTERNS: readonly RegExp[] = [\n /ChunkLoadError/i,\n /Loading chunk [\\w-]+ failed/i,\n /Loading CSS chunk/i,\n /Failed to fetch dynamically imported module/i,\n /error loading dynamically imported module/i,\n /Importing a module script failed/i,\n /is not a valid JavaScript MIME type/i,\n];\n\n/** Injectable side-effect ports; default to the real `window`. */\nexport interface ChunkRecoveryPorts {\n hasFlag: () => boolean;\n setFlag: () => void;\n clearFlag: () => void;\n reload: () => void;\n}\n\n/** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */\nexport function isChunkLoadError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if (error.name === 'ChunkLoadError') {\n return true;\n }\n const haystack = `${error.name} ${error.message}`;\n return CHUNK_ERROR_PATTERNS.some((pattern) => pattern.test(haystack));\n}\n\nfunction resolveStorage(): ChunkRecoveryStorage | undefined {\n if (typeof window === 'undefined') {\n return undefined;\n }\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\nfunction defaultPorts(): ChunkRecoveryPorts {\n const storage = resolveStorage();\n const hasWindow = typeof window !== 'undefined';\n return {\n hasFlag: () => Boolean(storage?.getItem(CHUNK_RELOAD_FLAG)),\n setFlag: (): void => {\n storage?.setItem(CHUNK_RELOAD_FLAG, '1');\n },\n clearFlag: (): void => {\n storage?.removeItem(CHUNK_RELOAD_FLAG);\n },\n reload: (): void => {\n if (hasWindow) {\n window.location.reload();\n }\n },\n };\n}\n\n/**\n * Attempt a one-shot reload to recover from a stale-chunk error.\n * Returns `true` when a reload was triggered, `false` when the one-shot guard was\n * already spent (the caller then shows a manual Reload action, never a loop).\n */\nexport function attemptChunkRecovery(ports: ChunkRecoveryPorts = defaultPorts()): boolean {\n if (ports.hasFlag()) {\n return false;\n }\n ports.setFlag();\n ports.reload();\n return true;\n}\n\n/** Clear the one-shot guard after a clean load so a FUTURE deploy can auto-recover. */\nexport function clearChunkRecoveryFlag(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.clearFlag();\n}\n\n/** Manually reload the page (the Reload action). */\nexport function reloadPage(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.reload();\n}\n","export { isValueDefined } from './isValueDefined';\r\nexport { isNotEmptyArray } from './isNotEmptyArray';\r\nexport { isNotEmptyString } from './isNotEmptyString';\r\nexport { isNullOrUndefined } from './isNullOrUndefined';\r\nexport { isEmptyArray } from './isEmptyArray';\r\nexport { isEmptyString } from './isEmptyString';\r\n","export { assertDefined } from './assertDefined';\r\n","export { getErrorMessage } from './getErrorMessage';\n","export { formatDate } from './formatDate';\n","export { toCsv } from './toCsv';\nexport type { CsvColumn } from './toCsv';\n","export { setRedirectHandler, redirectTo, resetRedirectHandler } from './redirect';\n","export {\n isChunkLoadError,\n attemptChunkRecovery,\n clearChunkRecoveryFlag,\n reloadPage,\n} from './chunkRecovery';\nexport type { ChunkRecoveryPorts } from './chunkRecovery';\n","export {\n sanitizeHtml,\n removeControlCharacters,\n sanitizeNotificationMessage,\n sanitizeText,\n sanitizeUrl,\n} from './sanitize';\n"]}
1
+ {"version":3,"sources":["../src/guards/isValueDefined.ts","../src/guards/isNotEmptyArray.ts","../src/guards/isNotEmptyString.ts","../src/guards/isNullOrUndefined.ts","../src/guards/isEmptyArray.ts","../src/guards/isEmptyString.ts","../src/assertions/assertDefined.ts","../src/async/fireAndForget.ts","../src/error/getErrorMessage.ts","../src/format/formatDate.ts","../src/csv/toCsv.ts","../src/sanitize/sanitize.ts","../src/navigation/redirect.ts","../src/chunkRecovery/chunkRecovery.ts","../src/guards/index.ts","../src/assertions/index.ts","../src/async/index.ts","../src/error/index.ts","../src/format/index.ts","../src/csv/index.ts","../src/navigation/index.ts","../src/chunkRecovery/index.ts","../src/sanitize/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * Type guard that checks if a value is neither null nor undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is defined (not null and not undefined)\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isValueDefined(value)) {\r\n * // value is narrowed to string\r\n * console.log(value.length);\r\n * }\r\n * ```\r\n */\r\nexport function isValueDefined<T>(value: T | null | undefined): value is T {\r\n return value !== undefined && value !== null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is a non-empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with at least one element\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] | undefined = getItems();\r\n * if (isNotEmptyArray(items)) {\r\n * // items is narrowed to string[] with at least one element\r\n * console.log(items[0]); // Safe to access\r\n * }\r\n * ```\r\n */\r\nexport function isNotEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: number } {\r\n return Array.isArray(value) && value.length > 0;\r\n}\r\n","/**\r\n * Checks if a value is a non-empty string (after trimming whitespace).\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is a string with non-whitespace content\r\n *\r\n * @example\r\n * ```typescript\r\n * isNotEmptyString('hello'); // true\r\n * isNotEmptyString(' '); // false\r\n * isNotEmptyString(''); // false\r\n * isNotEmptyString(undefined); // false\r\n * ```\r\n */\r\nexport function isNotEmptyString(value: string | null | undefined): value is string {\r\n return typeof value === 'string' && value.trim().length > 0;\r\n}\r\n","/**\r\n * Type guard that checks if a value is null or undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isNullOrUndefined(value)) {\r\n * // value is narrowed to null | undefined\r\n * console.log('Value is not set');\r\n * }\r\n * ```\r\n */\r\nexport function isNullOrUndefined(value: unknown): value is null | undefined {\r\n return value === undefined || value === null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is an empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with zero elements\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] = getItems();\r\n * if (isEmptyArray(items)) {\r\n * console.log('No items found');\r\n * }\r\n * ```\r\n */\r\nexport function isEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: 0 } {\r\n return Array.isArray(value) && value.length === 0;\r\n}\r\n","/**\r\n * Checks if a value is an empty string or contains only whitespace.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an empty string or whitespace-only\r\n *\r\n * @example\r\n * ```typescript\r\n * isEmptyString(''); // true\r\n * isEmptyString(' '); // true\r\n * isEmptyString('hello'); // false\r\n * isEmptyString(undefined); // false (not a string)\r\n * isEmptyString(null); // false (not a string)\r\n * ```\r\n */\r\nexport function isEmptyString(value: string | null | undefined): boolean {\r\n return typeof value === 'string' && value.trim().length === 0;\r\n}\r\n","/**\r\n * Asserts that a value is defined (not null or undefined).\r\n * Throws an error if the value is null or undefined.\r\n *\r\n * @param value - The value to assert\r\n * @param message - Optional error message\r\n * @throws Error if value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = getUser(); // User | null\r\n * assertDefined(user, 'User must exist');\r\n * // user is now narrowed to User\r\n * console.log(user.name);\r\n * ```\r\n */\r\nexport function assertDefined<T>(\r\n value: T | null | undefined,\r\n message?: string\r\n): asserts value is T {\r\n if (value === undefined || value === null) {\r\n throw new Error(message ?? 'Value must be defined');\r\n }\r\n}\r\n","/** Starts a promise without awaiting it and swallows its rejection. */\nexport function fireAndForget(promise: Promise<unknown>): void {\n promise.catch(() => undefined);\n}\n","import { isValueDefined } from '../guards/isValueDefined';\n\ninterface ErrorWithMessage {\n message?: unknown;\n}\n\nfunction isErrorWithMessage(value: unknown): value is ErrorWithMessage {\n return typeof value === 'object' && isValueDefined(value) && 'message' in value;\n}\n\n/**\n * Extracts a human-readable error message from various error shapes\n * (Error, string, or an object with a `message` property).\n *\n * @param value - The error value.\n * @param fallback - Returned when no message can be extracted (default: 'Unknown error').\n */\nexport function getErrorMessage(value: unknown, fallback = 'Unknown error'): string {\n if (value instanceof Error) {return value.message;}\n if (typeof value === 'string') {return value;}\n if (isErrorWithMessage(value)) {\n const message = value.message;\n if (typeof message === 'string' && message.length > 0) {return message;}\n }\n return fallback;\n}\n","/**\n * Locale-aware date formatting (pure — the caller supplies the locale, e.g. from i18n).\n * Returns '' for null/undefined; falls back to the ISO date if Intl formatting throws.\n *\n * @example formatDate(new Date(), 'en') // \"6/15/2024\"\n * @example formatDate(new Date(), 'en', { year: 'numeric', month: 'long' }) // \"June 2024\"\n * @example formatDate(null, 'en') // \"\"\n */\nexport function formatDate(\n date: Date | null | undefined,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n if (date === null || date === undefined) {return '';}\n\n const resolvedLocale = locale !== '' ? locale : 'en';\n\n try {\n return new Intl.DateTimeFormat(resolvedLocale, options).format(date);\n } catch {\n // Fallback to ISO date if formatting fails (invalid locale/options).\n return date.toISOString().split('T')[0] ?? '';\n }\n}\n","/**\n * RFC 4180 CSV serialization — the pure, DOM-free core of \"export the rows on screen to CSV\".\n *\n * The escaping is the whole risk surface: a comma, a quote or a newline inside a value (a\n * counterparty name, an address) is exactly what turns one row into two columns or two rows in a\n * spreadsheet. So the serializer lives here as a pure, total function a unit test can hammer, split\n * from any browser download shell (which is a side-effecting, app-owned concern).\n *\n * Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) — the escaping logic is universal across\n * the fleet; the DOM `<a download>` shell that consumes it stays in the app until a 2nd consumer.\n */\nimport { isValueDefined } from '../guards/isValueDefined';\n\n/** One column of a CSV export: a (caller-localized) header and how to read the cell from a row. */\nexport interface CsvColumn<T> {\n /** The column header — already localized by the caller. */\n header: string;\n /** Reads the cell value from a row. `null`/`undefined` become an empty cell. */\n value: (row: T) => string | number | null | undefined;\n}\n\n/** RFC 4180: a field must be quoted when it contains a quote, a comma, or a line break. */\nconst NEEDS_QUOTING = /[\"',\\r\\n]/;\n\n// A more permissive test than strictly required (it also quotes apostrophes), so a value like\n// O'Brien is never split by a spreadsheet that treats a leading quote specially — harmless, and it\n// keeps the rule to one regex.\nconst QUOTE = '\"';\nconst ESCAPED_QUOTE = '\"\"';\n/** Every embedded quote, so each is doubled per RFC 4180 (ES2020-safe; no `replaceAll`). */\nconst EVERY_QUOTE = /\"/g;\n\n/** RFC 4180 uses CRLF between records; Excel and Sheets both require it. */\nconst ROW_SEPARATOR = '\\r\\n';\nconst FIELD_SEPARATOR = ',';\n\n/** Quote and escape a single field per RFC 4180. `null`/`undefined` → an empty field. */\nfunction escapeField(raw: string | number | null | undefined): string {\n const text = isValueDefined(raw) ? String(raw) : '';\n if (!NEEDS_QUOTING.test(text)) {\n return text;\n }\n return `${QUOTE}${text.replace(EVERY_QUOTE, ESCAPED_QUOTE)}${QUOTE}`;\n}\n\n/**\n * Serialize rows to an RFC 4180 CSV string — a header row followed by one row per item.\n *\n * Pure and total: an empty `rows` yields the header line alone, which opens as an empty-but-labelled\n * spreadsheet rather than a zero-byte file the operator cannot tell apart from a failure.\n */\nexport function toCsv<T>(rows: readonly T[], columns: ReadonlyArray<CsvColumn<T>>): string {\n const headerLine = columns.map((column) => escapeField(column.header)).join(FIELD_SEPARATOR);\n\n const dataLines = rows.map((row) =>\n columns.map((column) => escapeField(column.value(row))).join(FIELD_SEPARATOR),\n );\n\n return [headerLine, ...dataLines].join(ROW_SEPARATOR);\n}\n","/**\n * Text sanitization utilities for preventing XSS and ensuring safe display.\n * These utilities sanitize user-provided text before rendering.\n */\n\n/**\n * HTML entities that should be escaped to prevent XSS attacks.\n */\nconst HTML_ENTITIES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#x27;',\n '/': '&#x2F;',\n '`': '&#x60;',\n '=': '&#x3D;',\n};\n\n/**\n * Regular expression matching characters that need escaping.\n */\nconst HTML_ESCAPE_REGEX = /[&<>\"'`=/]/g;\n\n/**\n * Unicode code point ranges for dangerous characters.\n * These are control characters and zero-width characters that can be\n * used in homograph attacks or to hide malicious content.\n */\n\n/** C0 control characters range start (NULL) */\nconst C0_CONTROL_START = 0x0000;\n/** C0 control characters range end (Unit Separator) */\nconst C0_CONTROL_END = 0x001f;\n/** C1 control characters range start (DEL and beyond) */\nconst C1_CONTROL_START = 0x007f;\n/** C1 control characters range end */\nconst C1_CONTROL_END = 0x009f;\n/** Zero-width characters range start (Zero Width Space) */\nconst ZERO_WIDTH_START = 0x200b;\n/** Zero-width characters range end (Right-to-Left Mark) */\nconst ZERO_WIDTH_END = 0x200f;\n/** Format characters range start (Line Separator) */\nconst FORMAT_CHARS_START = 0x2028;\n/** Format characters range end (Narrow No-Break Space) */\nconst FORMAT_CHARS_END = 0x202f;\n/** Byte Order Mark code point */\nconst BYTE_ORDER_MARK = 0xfeff;\n\n/**\n * Maximum reasonable length for notification messages.\n */\nconst MAX_NOTIFICATION_LENGTH = 500;\n\n/**\n * Length of ellipsis suffix (\"...\") used when truncating text.\n */\nconst ELLIPSIS_LENGTH = 3;\n\n/**\n * Escapes HTML special characters in a string to prevent XSS attacks.\n * Use this when displaying user-provided content in the UI.\n *\n * @param text - The text to sanitize\n * @returns The sanitized text with HTML entities escaped\n *\n * @example\n * sanitizeHtml('<script>alert(\"xss\")</script>')\n * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'\n */\nexport function sanitizeHtml(text: string): string {\n if (text.length === 0) {return '';}\n return text.replace(HTML_ESCAPE_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Control characters and zero-width characters that should be removed.\n * These can be used in homograph attacks or to hide malicious content.\n */\nfunction isDangerousCharCode(code: number): boolean {\n const isC0Control = code >= C0_CONTROL_START && code <= C0_CONTROL_END;\n const isC1Control = code >= C1_CONTROL_START && code <= C1_CONTROL_END;\n const isZeroWidth = code >= ZERO_WIDTH_START && code <= ZERO_WIDTH_END;\n const isFormatChar = code >= FORMAT_CHARS_START && code <= FORMAT_CHARS_END;\n const isBom = code === BYTE_ORDER_MARK;\n return isC0Control || isC1Control || isZeroWidth || isFormatChar || isBom;\n}\n\n/**\n * Removes control characters and zero-width characters from text.\n * These characters can be used to create misleading text or hide content.\n *\n * @param text - The text to sanitize\n * @returns The text with dangerous characters removed\n */\nexport function removeControlCharacters(text: string): string {\n if (text.length === 0) {return '';}\n let out = '';\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (!isDangerousCharCode(code)) {out += text[i];}\n }\n return out;\n}\n\n/**\n * Sanitizes a notification message for safe display.\n * This function:\n * 1. Removes control/zero-width characters\n * 2. Trims whitespace\n * 3. Truncates to a reasonable length\n * 4. Escapes HTML entities (for web platform)\n *\n * Note: React Native's Text component already escapes HTML by default,\n * but this provides defense-in-depth and handles the web platform.\n *\n * @param message - The notification message to sanitize\n * @param maxLength - Maximum length (default: 500)\n * @returns The sanitized message\n *\n * @example\n * sanitizeNotificationMessage(' Hello <b>World</b> ')\n * // Returns: 'Hello &lt;b&gt;World&lt;/b&gt;'\n */\nexport function sanitizeNotificationMessage(\n message: string,\n maxLength: number = MAX_NOTIFICATION_LENGTH\n): string {\n if (message.length === 0) {return '';}\n\n // Remove dangerous characters first\n let sanitized = removeControlCharacters(message);\n\n // Trim whitespace\n sanitized = sanitized.trim();\n\n // Truncate if too long\n if (sanitized.length > maxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n // Escape HTML entities\n sanitized = sanitizeHtml(sanitized);\n\n return sanitized;\n}\n\n/**\n * Sanitizes text for safe display, without HTML escaping.\n * Use this for React Native components where HTML escaping isn't needed\n * but control character removal and length limits are still important.\n *\n * @param text - The text to sanitize\n * @param maxLength - Maximum length (optional)\n * @returns The sanitized text\n */\nexport function sanitizeText(text: string, maxLength?: number): string {\n if (text.length === 0) {return '';}\n\n let sanitized = removeControlCharacters(text);\n sanitized = sanitized.trim();\n\n const hasValidMaxLength = typeof maxLength === 'number' && maxLength > 0;\n const exceedsMaxLength = hasValidMaxLength && sanitized.length > maxLength;\n if (exceedsMaxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n return sanitized;\n}\n\n/**\n * Validates and sanitizes a URL to prevent javascript: and data: protocol attacks.\n *\n * @param url - The URL to validate\n * @returns The sanitized URL or empty string if invalid\n */\nexport function sanitizeUrl(url: string): string {\n if (url.length === 0) {return '';}\n\n const trimmedLower = url.trim().toLowerCase();\n const protocol = trimmedLower.split(':')[0];\n\n // Block dangerous protocols\n const isDangerousProtocol = protocol === 'javascript' || protocol === 'data' || protocol === 'vbscript';\n if (isDangerousProtocol) \n {return '';}\n \n\n return url.trim();\n}\n","/**\n * Framework-agnostic redirect escape hatch.\n *\n * WHY THIS EXISTS: non-React code (an axios 401 interceptor, an auth provider's\n * token-refresh failure path) needs to navigate, but has no access to a router\n * hook. The app registers its router's push/replace once via `setRedirectHandler`;\n * everything else calls `redirectTo` and does not care which router is mounted.\n *\n * Three layers of fallback, in order:\n * 1. the registered handler (the app's router),\n * 2. `window.location.replace` if no handler is registered yet,\n * 3. a queue, drained on the next `setRedirectHandler` call — so a redirect fired\n * before the router mounts is not silently dropped.\n *\n * Layer 1 additionally schedules a watchdog: some routers accept a `push` for a\n * route they cannot resolve and leave the URL untouched, which strands the user on\n * the page they were supposed to leave. If the location has not changed after\n * `NAVIGATION_CHECK_DELAY_MS`, a hard `window.location.replace` is forced.\n *\n * Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and\n * katalogos-web. Pure DOM + timers — no React, no router, no framework import.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020`. Adding DOM globally would let every\n * other util in the package reach for browser globals that are absent at runtime\n * on the server. Declaring the ~4 properties used here keeps that door shut.\n */\ninterface BrowserWindow {\n location: {\n pathname: string;\n search: string;\n hash: string;\n replace: (url: string) => void;\n };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** Delay in ms before checking whether the registered handler actually navigated. */\nconst NAVIGATION_CHECK_DELAY_MS = 150;\n\nlet redirectHandler: ((path: string) => void) | null = null;\nconst queuedRedirects: string[] = [];\n\n/**\n * Strips Expo-Router group segments — `/(protected)/settings` → `/settings`.\n * Group folders are a routing-config device and never appear in a real URL, so a\n * raw group path handed to `window.location` would 404.\n */\nfunction normalizeRouterPath(path: string): string {\n const normalized = path.replace(/\\/\\([^/]+\\)/g, '');\n return normalized === '' ? '/' : normalized;\n}\n\n/** Current location as path+search+hash, or null outside a browser (SSR/native). */\nfunction getCurrentLocation(): string | null {\n if (typeof window === 'undefined') {return null;}\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\n/** Force a hard navigation if the registered handler left the location unchanged. */\nfunction scheduleFallbackNavigation(before: string, normalized: string): void {\n setTimeout(() => {\n try {\n const after = getCurrentLocation();\n if (after === before && typeof window !== 'undefined') {window.location.replace(normalized);}\n } catch {\n /* ignore — window may have gone away (unmount/teardown) */\n }\n }, NAVIGATION_CHECK_DELAY_MS);\n}\n\n/** Attempt a hard browser navigation. Returns false outside a browser or on throw. */\nfunction tryWindowNavigation(normalized: string): boolean {\n if (typeof window === 'undefined') {return false;}\n try {\n window.location.replace(normalized);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Register the app's router navigate function and drain any redirects that were\n * queued before it mounted. Call once, from the root layout.\n */\nexport function setRedirectHandler(h: (path: string) => void): void {\n redirectHandler = h;\n while (queuedRedirects.length > 0) {\n const p = queuedRedirects.shift();\n try {\n if (typeof p === 'string' && p.length > 0) {h(normalizeRouterPath(p));}\n } catch {\n /* swallow; the handler may still not be ready */\n }\n }\n}\n\n/**\n * Navigate to `path` from anywhere, including outside React.\n * Safe to call before the router mounts — the redirect is queued and replayed.\n */\nexport function redirectTo(path: string): void {\n const normalized = normalizeRouterPath(path);\n if (redirectHandler) {\n const before = getCurrentLocation();\n try {\n redirectHandler(normalized);\n if (typeof before === 'string') {scheduleFallbackNavigation(before, normalized);}\n return;\n } catch {\n /* fall through to a hard navigation */\n }\n }\n if (tryWindowNavigation(normalized)) {return;}\n queuedRedirects.push(normalized);\n}\n\n/**\n * Clear the registered handler and any queued redirects.\n * Exists for tests — module state otherwise leaks between cases.\n */\nexport function resetRedirectHandler(): void {\n redirectHandler = null;\n queuedRedirects.length = 0;\n}\n","/**\n * Detection + one-shot auto-recovery for stale-chunk load failures.\n *\n * WHY (UX Move 3, would have masked P0-01): after a deploy, the browser may hold\n * a stale `index.html` that references hashed JS chunks which now 404 — surfacing\n * as a `ChunkLoadError` / \"failed to fetch dynamically imported module\". A single\n * guarded `location.reload()` re-fetches the fresh entry + chunks and the app\n * recovers with no user-visible error. The sessionStorage one-shot flag prevents\n * a reload LOOP if the reload does not fix it (then the boundary shows a manual\n * Reload action instead).\n *\n * Pure + port-injectable: detection is a pure predicate; the storage + reload\n * side effects are ports (default to `window`) so unit tests run deterministically\n * in jsdom without a real navigation.\n *\n * Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in\n * kefi-web, erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd).\n * The original file's own header predicted this move. Pairs with\n * `<AppErrorBoundary>` from `@dloizides/ui-feedback`, which consumes these as its\n * `retryable` / `onMount` / `recover` injections.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020` (see `navigation/redirect.ts`, which\n * makes the same call). Declaring the few properties used here keeps every other\n * util in the package from reaching for browser globals absent on the server.\n */\ninterface ChunkRecoveryStorage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n removeItem: (key: string) => void;\n}\n\ninterface BrowserWindow {\n sessionStorage: ChunkRecoveryStorage;\n location: { reload: () => void };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/**\n * sessionStorage key holding the epoch-ms timestamp of the last auto-reload attempt.\n *\n * A TIMESTAMP, not the old boolean `ui.chunkReload.attempted`. The boolean was released\n * by the consumer on a clean mount, and a lazy route makes the mount clean: the root\n * boundary commits (a Suspense fallback renders), `componentDidMount` releases the guard,\n * and only THEN does the dynamic import reject with a 404 chunk. Recovery re-armed itself\n * on every pass, so the reload loop the guard exists to bound ran unbounded.\n */\nconst CHUNK_RELOAD_AT = 'ui.chunkReload.attemptedAt';\n\n/**\n * How long one recorded attempt keeps blocking a second automatic reload.\n *\n * WHY A WINDOW RATHER THAN A COUNTER: attempt-counting bounds the loop too, but a counter\n * has to be reset by somebody, and the only place a consumer can put that reset is a clean\n * mount — the exact release that caused this defect. A window expires by itself, so there is\n * no release call to fire at the wrong moment. One minute is far longer than a reload-plus-boot\n * cycle (single-digit seconds, which is what the loop runs at) and far shorter than the gap to\n * a genuinely later, unrelated rollout, which is still allowed to auto-recover once.\n */\nconst RECOVERY_COOLDOWN_MS = 60_000;\n\n/** Messages/names emitted by webpack/metro/vite/Safari for a stale-chunk failure. */\nconst CHUNK_ERROR_PATTERNS: readonly RegExp[] = [\n /ChunkLoadError/i,\n /Loading chunk [\\w-]+ failed/i,\n /Loading CSS chunk/i,\n /Failed to fetch dynamically imported module/i,\n /error loading dynamically imported module/i,\n /Importing a module script failed/i,\n /is not a valid JavaScript MIME type/i,\n];\n\n/** Injectable side-effect ports; default to the real `window`. */\nexport interface ChunkRecoveryPorts {\n /** Epoch-ms of the last recorded attempt, or `null` when none is stored. */\n readAttemptAt: () => number | null;\n recordAttemptAt: (at: number) => void;\n clearAttempt: () => void;\n reload: () => void;\n now: () => number;\n}\n\n/** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */\nexport function isChunkLoadError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if (error.name === 'ChunkLoadError') {\n return true;\n }\n const haystack = `${error.name} ${error.message}`;\n return CHUNK_ERROR_PATTERNS.some((pattern) => pattern.test(haystack));\n}\n\nfunction resolveStorage(): ChunkRecoveryStorage | undefined {\n if (typeof window === 'undefined') {\n return undefined;\n }\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\nfunction defaultPorts(): ChunkRecoveryPorts {\n const storage = resolveStorage();\n const hasWindow = typeof window !== 'undefined';\n return {\n readAttemptAt: (): number | null => {\n const raw = storage?.getItem(CHUNK_RELOAD_AT);\n if (raw === null || raw === undefined || raw === '') {\n return null;\n }\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : null;\n },\n recordAttemptAt: (at: number): void => {\n storage?.setItem(CHUNK_RELOAD_AT, String(at));\n },\n clearAttempt: (): void => {\n storage?.removeItem(CHUNK_RELOAD_AT);\n },\n now: (): number => Date.now(),\n reload: (): void => {\n if (hasWindow) {\n window.location.reload();\n }\n },\n };\n}\n\n/** True when a recorded attempt is still inside the cooldown window. */\nfunction isWithinCooldown(attemptedAt: number | null, now: number): boolean {\n return attemptedAt !== null && now - attemptedAt < RECOVERY_COOLDOWN_MS;\n}\n\n/**\n * Attempt a guarded reload to recover from a stale-chunk error.\n * Returns `true` when a reload was triggered, `false` while the cooldown from the previous\n * attempt is still running (the caller then shows a manual Reload action, never a loop).\n *\n * `recordAttemptAt` and `reload` stay synchronous and adjacent so the record survives the\n * navigation that follows.\n */\nexport function attemptChunkRecovery(ports: ChunkRecoveryPorts = defaultPorts()): boolean {\n const now = ports.now();\n if (isWithinCooldown(ports.readAttemptAt(), now)) {\n return false;\n }\n ports.recordAttemptAt(now);\n ports.reload();\n return true;\n}\n\n/**\n * Release the guard after a clean load — but ONLY once the cooldown has expired.\n *\n * Consumers call this from the root boundary's clean-mount hook, and on a lazy route that\n * mount happens BEFORE the chunk rejects. Refusing to clear a live record is what makes the\n * bound hold no matter when the consumer calls this: an expired record permits recovery\n * anyway, so clearing it only tidies storage, while a live one is left alone.\n */\nexport function clearChunkRecoveryFlag(ports: ChunkRecoveryPorts = defaultPorts()): void {\n if (isWithinCooldown(ports.readAttemptAt(), ports.now())) {\n return;\n }\n ports.clearAttempt();\n}\n\n/** Manually reload the page (the Reload action). */\nexport function reloadPage(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.reload();\n}\n","export { isValueDefined } from './isValueDefined';\r\nexport { isNotEmptyArray } from './isNotEmptyArray';\r\nexport { isNotEmptyString } from './isNotEmptyString';\r\nexport { isNullOrUndefined } from './isNullOrUndefined';\r\nexport { isEmptyArray } from './isEmptyArray';\r\nexport { isEmptyString } from './isEmptyString';\r\n","export { assertDefined } from './assertDefined';\r\n","export { fireAndForget } from './fireAndForget';\n","export { getErrorMessage } from './getErrorMessage';\n","export { formatDate } from './formatDate';\n","export { toCsv } from './toCsv';\nexport type { CsvColumn } from './toCsv';\n","export { setRedirectHandler, redirectTo, resetRedirectHandler } from './redirect';\n","export {\n isChunkLoadError,\n attemptChunkRecovery,\n clearChunkRecoveryFlag,\n reloadPage,\n} from './chunkRecovery';\nexport type { ChunkRecoveryPorts } from './chunkRecovery';\n","export {\n sanitizeHtml,\n removeControlCharacters,\n sanitizeNotificationMessage,\n sanitizeText,\n sanitizeUrl,\n} from './sanitize';\n"]}
package/dist/index.mjs CHANGED
@@ -41,6 +41,11 @@ function assertDefined(value, message) {
41
41
  }
42
42
  }
43
43
 
44
+ // src/async/fireAndForget.ts
45
+ function fireAndForget(promise) {
46
+ promise.catch(() => void 0);
47
+ }
48
+
44
49
  // src/error/getErrorMessage.ts
45
50
  function isErrorWithMessage(value) {
46
51
  return typeof value === "object" && isValueDefined(value) && "message" in value;
@@ -256,7 +261,8 @@ function resetRedirectHandler() {
256
261
  }
257
262
 
258
263
  // src/chunkRecovery/chunkRecovery.ts
259
- var CHUNK_RELOAD_FLAG = "ui.chunkReload.attempted";
264
+ var CHUNK_RELOAD_AT = "ui.chunkReload.attemptedAt";
265
+ var RECOVERY_COOLDOWN_MS = 6e4;
260
266
  var CHUNK_ERROR_PATTERNS = [
261
267
  /ChunkLoadError/i,
262
268
  /Loading chunk [\w-]+ failed/i,
@@ -290,13 +296,21 @@ function defaultPorts() {
290
296
  const storage = resolveStorage();
291
297
  const hasWindow = typeof window !== "undefined";
292
298
  return {
293
- hasFlag: () => Boolean(storage?.getItem(CHUNK_RELOAD_FLAG)),
294
- setFlag: () => {
295
- storage?.setItem(CHUNK_RELOAD_FLAG, "1");
299
+ readAttemptAt: () => {
300
+ const raw = storage?.getItem(CHUNK_RELOAD_AT);
301
+ if (raw === null || raw === void 0 || raw === "") {
302
+ return null;
303
+ }
304
+ const parsed = Number(raw);
305
+ return Number.isFinite(parsed) ? parsed : null;
296
306
  },
297
- clearFlag: () => {
298
- storage?.removeItem(CHUNK_RELOAD_FLAG);
307
+ recordAttemptAt: (at) => {
308
+ storage?.setItem(CHUNK_RELOAD_AT, String(at));
299
309
  },
310
+ clearAttempt: () => {
311
+ storage?.removeItem(CHUNK_RELOAD_AT);
312
+ },
313
+ now: () => Date.now(),
300
314
  reload: () => {
301
315
  if (hasWindow) {
302
316
  window.location.reload();
@@ -304,16 +318,23 @@ function defaultPorts() {
304
318
  }
305
319
  };
306
320
  }
321
+ function isWithinCooldown(attemptedAt, now) {
322
+ return attemptedAt !== null && now - attemptedAt < RECOVERY_COOLDOWN_MS;
323
+ }
307
324
  function attemptChunkRecovery(ports = defaultPorts()) {
308
- if (ports.hasFlag()) {
325
+ const now = ports.now();
326
+ if (isWithinCooldown(ports.readAttemptAt(), now)) {
309
327
  return false;
310
328
  }
311
- ports.setFlag();
329
+ ports.recordAttemptAt(now);
312
330
  ports.reload();
313
331
  return true;
314
332
  }
315
333
  function clearChunkRecoveryFlag(ports = defaultPorts()) {
316
- ports.clearFlag();
334
+ if (isWithinCooldown(ports.readAttemptAt(), ports.now())) {
335
+ return;
336
+ }
337
+ ports.clearAttempt();
317
338
  }
318
339
  function reloadPage(ports = defaultPorts()) {
319
340
  ports.reload();
@@ -336,6 +357,12 @@ __export(assertions_exports, {
336
357
  assertDefined: () => assertDefined
337
358
  });
338
359
 
360
+ // src/async/index.ts
361
+ var async_exports = {};
362
+ __export(async_exports, {
363
+ fireAndForget: () => fireAndForget
364
+ });
365
+
339
366
  // src/error/index.ts
340
367
  var error_exports = {};
341
368
  __export(error_exports, {
@@ -381,6 +408,6 @@ __export(sanitize_exports, {
381
408
  sanitizeUrl: () => sanitizeUrl
382
409
  });
383
410
 
384
- export { assertDefined, assertions_exports as assertions, attemptChunkRecovery, chunkRecovery_exports as chunkRecovery, clearChunkRecoveryFlag, csv_exports as csv, error_exports as error, format_exports as format, formatDate, getErrorMessage, guards_exports as guards, isChunkLoadError, isEmptyArray, isEmptyString, isNotEmptyArray, isNotEmptyString, isNullOrUndefined, isValueDefined, navigation_exports as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, sanitize_exports as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler, toCsv };
411
+ export { assertDefined, assertions_exports as assertions, async_exports as async, attemptChunkRecovery, chunkRecovery_exports as chunkRecovery, clearChunkRecoveryFlag, csv_exports as csv, error_exports as error, fireAndForget, format_exports as format, formatDate, getErrorMessage, guards_exports as guards, isChunkLoadError, isEmptyArray, isEmptyString, isNotEmptyArray, isNotEmptyString, isNullOrUndefined, isValueDefined, navigation_exports as navigation, redirectTo, reloadPage, removeControlCharacters, resetRedirectHandler, sanitize_exports as sanitize, sanitizeHtml, sanitizeNotificationMessage, sanitizeText, sanitizeUrl, setRedirectHandler, toCsv };
385
412
  //# sourceMappingURL=index.mjs.map
386
413
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/guards/isValueDefined.ts","../src/guards/isNotEmptyArray.ts","../src/guards/isNotEmptyString.ts","../src/guards/isNullOrUndefined.ts","../src/guards/isEmptyArray.ts","../src/guards/isEmptyString.ts","../src/assertions/assertDefined.ts","../src/error/getErrorMessage.ts","../src/format/formatDate.ts","../src/csv/toCsv.ts","../src/sanitize/sanitize.ts","../src/navigation/redirect.ts","../src/chunkRecovery/chunkRecovery.ts","../src/guards/index.ts","../src/assertions/index.ts","../src/error/index.ts","../src/format/index.ts","../src/csv/index.ts","../src/navigation/index.ts","../src/chunkRecovery/index.ts","../src/sanitize/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\r\n * Type guard that checks if a value is neither null nor undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is defined (not null and not undefined)\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isValueDefined(value)) {\r\n * // value is narrowed to string\r\n * console.log(value.length);\r\n * }\r\n * ```\r\n */\r\nexport function isValueDefined<T>(value: T | null | undefined): value is T {\r\n return value !== undefined && value !== null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is a non-empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with at least one element\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] | undefined = getItems();\r\n * if (isNotEmptyArray(items)) {\r\n * // items is narrowed to string[] with at least one element\r\n * console.log(items[0]); // Safe to access\r\n * }\r\n * ```\r\n */\r\nexport function isNotEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: number } {\r\n return Array.isArray(value) && value.length > 0;\r\n}\r\n","/**\r\n * Checks if a value is a non-empty string (after trimming whitespace).\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is a string with non-whitespace content\r\n *\r\n * @example\r\n * ```typescript\r\n * isNotEmptyString('hello'); // true\r\n * isNotEmptyString(' '); // false\r\n * isNotEmptyString(''); // false\r\n * isNotEmptyString(undefined); // false\r\n * ```\r\n */\r\nexport function isNotEmptyString(value: string | null | undefined): value is string {\r\n return typeof value === 'string' && value.trim().length > 0;\r\n}\r\n","/**\r\n * Type guard that checks if a value is null or undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isNullOrUndefined(value)) {\r\n * // value is narrowed to null | undefined\r\n * console.log('Value is not set');\r\n * }\r\n * ```\r\n */\r\nexport function isNullOrUndefined(value: unknown): value is null | undefined {\r\n return value === undefined || value === null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is an empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with zero elements\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] = getItems();\r\n * if (isEmptyArray(items)) {\r\n * console.log('No items found');\r\n * }\r\n * ```\r\n */\r\nexport function isEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: 0 } {\r\n return Array.isArray(value) && value.length === 0;\r\n}\r\n","/**\r\n * Checks if a value is an empty string or contains only whitespace.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an empty string or whitespace-only\r\n *\r\n * @example\r\n * ```typescript\r\n * isEmptyString(''); // true\r\n * isEmptyString(' '); // true\r\n * isEmptyString('hello'); // false\r\n * isEmptyString(undefined); // false (not a string)\r\n * isEmptyString(null); // false (not a string)\r\n * ```\r\n */\r\nexport function isEmptyString(value: string | null | undefined): boolean {\r\n return typeof value === 'string' && value.trim().length === 0;\r\n}\r\n","/**\r\n * Asserts that a value is defined (not null or undefined).\r\n * Throws an error if the value is null or undefined.\r\n *\r\n * @param value - The value to assert\r\n * @param message - Optional error message\r\n * @throws Error if value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = getUser(); // User | null\r\n * assertDefined(user, 'User must exist');\r\n * // user is now narrowed to User\r\n * console.log(user.name);\r\n * ```\r\n */\r\nexport function assertDefined<T>(\r\n value: T | null | undefined,\r\n message?: string\r\n): asserts value is T {\r\n if (value === undefined || value === null) {\r\n throw new Error(message ?? 'Value must be defined');\r\n }\r\n}\r\n","import { isValueDefined } from '../guards/isValueDefined';\n\ninterface ErrorWithMessage {\n message?: unknown;\n}\n\nfunction isErrorWithMessage(value: unknown): value is ErrorWithMessage {\n return typeof value === 'object' && isValueDefined(value) && 'message' in value;\n}\n\n/**\n * Extracts a human-readable error message from various error shapes\n * (Error, string, or an object with a `message` property).\n *\n * @param value - The error value.\n * @param fallback - Returned when no message can be extracted (default: 'Unknown error').\n */\nexport function getErrorMessage(value: unknown, fallback = 'Unknown error'): string {\n if (value instanceof Error) {return value.message;}\n if (typeof value === 'string') {return value;}\n if (isErrorWithMessage(value)) {\n const message = value.message;\n if (typeof message === 'string' && message.length > 0) {return message;}\n }\n return fallback;\n}\n","/**\n * Locale-aware date formatting (pure — the caller supplies the locale, e.g. from i18n).\n * Returns '' for null/undefined; falls back to the ISO date if Intl formatting throws.\n *\n * @example formatDate(new Date(), 'en') // \"6/15/2024\"\n * @example formatDate(new Date(), 'en', { year: 'numeric', month: 'long' }) // \"June 2024\"\n * @example formatDate(null, 'en') // \"\"\n */\nexport function formatDate(\n date: Date | null | undefined,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n if (date === null || date === undefined) {return '';}\n\n const resolvedLocale = locale !== '' ? locale : 'en';\n\n try {\n return new Intl.DateTimeFormat(resolvedLocale, options).format(date);\n } catch {\n // Fallback to ISO date if formatting fails (invalid locale/options).\n return date.toISOString().split('T')[0] ?? '';\n }\n}\n","/**\n * RFC 4180 CSV serialization — the pure, DOM-free core of \"export the rows on screen to CSV\".\n *\n * The escaping is the whole risk surface: a comma, a quote or a newline inside a value (a\n * counterparty name, an address) is exactly what turns one row into two columns or two rows in a\n * spreadsheet. So the serializer lives here as a pure, total function a unit test can hammer, split\n * from any browser download shell (which is a side-effecting, app-owned concern).\n *\n * Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) — the escaping logic is universal across\n * the fleet; the DOM `<a download>` shell that consumes it stays in the app until a 2nd consumer.\n */\nimport { isValueDefined } from '../guards/isValueDefined';\n\n/** One column of a CSV export: a (caller-localized) header and how to read the cell from a row. */\nexport interface CsvColumn<T> {\n /** The column header — already localized by the caller. */\n header: string;\n /** Reads the cell value from a row. `null`/`undefined` become an empty cell. */\n value: (row: T) => string | number | null | undefined;\n}\n\n/** RFC 4180: a field must be quoted when it contains a quote, a comma, or a line break. */\nconst NEEDS_QUOTING = /[\"',\\r\\n]/;\n\n// A more permissive test than strictly required (it also quotes apostrophes), so a value like\n// O'Brien is never split by a spreadsheet that treats a leading quote specially — harmless, and it\n// keeps the rule to one regex.\nconst QUOTE = '\"';\nconst ESCAPED_QUOTE = '\"\"';\n/** Every embedded quote, so each is doubled per RFC 4180 (ES2020-safe; no `replaceAll`). */\nconst EVERY_QUOTE = /\"/g;\n\n/** RFC 4180 uses CRLF between records; Excel and Sheets both require it. */\nconst ROW_SEPARATOR = '\\r\\n';\nconst FIELD_SEPARATOR = ',';\n\n/** Quote and escape a single field per RFC 4180. `null`/`undefined` → an empty field. */\nfunction escapeField(raw: string | number | null | undefined): string {\n const text = isValueDefined(raw) ? String(raw) : '';\n if (!NEEDS_QUOTING.test(text)) {\n return text;\n }\n return `${QUOTE}${text.replace(EVERY_QUOTE, ESCAPED_QUOTE)}${QUOTE}`;\n}\n\n/**\n * Serialize rows to an RFC 4180 CSV string — a header row followed by one row per item.\n *\n * Pure and total: an empty `rows` yields the header line alone, which opens as an empty-but-labelled\n * spreadsheet rather than a zero-byte file the operator cannot tell apart from a failure.\n */\nexport function toCsv<T>(rows: readonly T[], columns: ReadonlyArray<CsvColumn<T>>): string {\n const headerLine = columns.map((column) => escapeField(column.header)).join(FIELD_SEPARATOR);\n\n const dataLines = rows.map((row) =>\n columns.map((column) => escapeField(column.value(row))).join(FIELD_SEPARATOR),\n );\n\n return [headerLine, ...dataLines].join(ROW_SEPARATOR);\n}\n","/**\n * Text sanitization utilities for preventing XSS and ensuring safe display.\n * These utilities sanitize user-provided text before rendering.\n */\n\n/**\n * HTML entities that should be escaped to prevent XSS attacks.\n */\nconst HTML_ENTITIES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#x27;',\n '/': '&#x2F;',\n '`': '&#x60;',\n '=': '&#x3D;',\n};\n\n/**\n * Regular expression matching characters that need escaping.\n */\nconst HTML_ESCAPE_REGEX = /[&<>\"'`=/]/g;\n\n/**\n * Unicode code point ranges for dangerous characters.\n * These are control characters and zero-width characters that can be\n * used in homograph attacks or to hide malicious content.\n */\n\n/** C0 control characters range start (NULL) */\nconst C0_CONTROL_START = 0x0000;\n/** C0 control characters range end (Unit Separator) */\nconst C0_CONTROL_END = 0x001f;\n/** C1 control characters range start (DEL and beyond) */\nconst C1_CONTROL_START = 0x007f;\n/** C1 control characters range end */\nconst C1_CONTROL_END = 0x009f;\n/** Zero-width characters range start (Zero Width Space) */\nconst ZERO_WIDTH_START = 0x200b;\n/** Zero-width characters range end (Right-to-Left Mark) */\nconst ZERO_WIDTH_END = 0x200f;\n/** Format characters range start (Line Separator) */\nconst FORMAT_CHARS_START = 0x2028;\n/** Format characters range end (Narrow No-Break Space) */\nconst FORMAT_CHARS_END = 0x202f;\n/** Byte Order Mark code point */\nconst BYTE_ORDER_MARK = 0xfeff;\n\n/**\n * Maximum reasonable length for notification messages.\n */\nconst MAX_NOTIFICATION_LENGTH = 500;\n\n/**\n * Length of ellipsis suffix (\"...\") used when truncating text.\n */\nconst ELLIPSIS_LENGTH = 3;\n\n/**\n * Escapes HTML special characters in a string to prevent XSS attacks.\n * Use this when displaying user-provided content in the UI.\n *\n * @param text - The text to sanitize\n * @returns The sanitized text with HTML entities escaped\n *\n * @example\n * sanitizeHtml('<script>alert(\"xss\")</script>')\n * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'\n */\nexport function sanitizeHtml(text: string): string {\n if (text.length === 0) {return '';}\n return text.replace(HTML_ESCAPE_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Control characters and zero-width characters that should be removed.\n * These can be used in homograph attacks or to hide malicious content.\n */\nfunction isDangerousCharCode(code: number): boolean {\n const isC0Control = code >= C0_CONTROL_START && code <= C0_CONTROL_END;\n const isC1Control = code >= C1_CONTROL_START && code <= C1_CONTROL_END;\n const isZeroWidth = code >= ZERO_WIDTH_START && code <= ZERO_WIDTH_END;\n const isFormatChar = code >= FORMAT_CHARS_START && code <= FORMAT_CHARS_END;\n const isBom = code === BYTE_ORDER_MARK;\n return isC0Control || isC1Control || isZeroWidth || isFormatChar || isBom;\n}\n\n/**\n * Removes control characters and zero-width characters from text.\n * These characters can be used to create misleading text or hide content.\n *\n * @param text - The text to sanitize\n * @returns The text with dangerous characters removed\n */\nexport function removeControlCharacters(text: string): string {\n if (text.length === 0) {return '';}\n let out = '';\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (!isDangerousCharCode(code)) {out += text[i];}\n }\n return out;\n}\n\n/**\n * Sanitizes a notification message for safe display.\n * This function:\n * 1. Removes control/zero-width characters\n * 2. Trims whitespace\n * 3. Truncates to a reasonable length\n * 4. Escapes HTML entities (for web platform)\n *\n * Note: React Native's Text component already escapes HTML by default,\n * but this provides defense-in-depth and handles the web platform.\n *\n * @param message - The notification message to sanitize\n * @param maxLength - Maximum length (default: 500)\n * @returns The sanitized message\n *\n * @example\n * sanitizeNotificationMessage(' Hello <b>World</b> ')\n * // Returns: 'Hello &lt;b&gt;World&lt;/b&gt;'\n */\nexport function sanitizeNotificationMessage(\n message: string,\n maxLength: number = MAX_NOTIFICATION_LENGTH\n): string {\n if (message.length === 0) {return '';}\n\n // Remove dangerous characters first\n let sanitized = removeControlCharacters(message);\n\n // Trim whitespace\n sanitized = sanitized.trim();\n\n // Truncate if too long\n if (sanitized.length > maxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n // Escape HTML entities\n sanitized = sanitizeHtml(sanitized);\n\n return sanitized;\n}\n\n/**\n * Sanitizes text for safe display, without HTML escaping.\n * Use this for React Native components where HTML escaping isn't needed\n * but control character removal and length limits are still important.\n *\n * @param text - The text to sanitize\n * @param maxLength - Maximum length (optional)\n * @returns The sanitized text\n */\nexport function sanitizeText(text: string, maxLength?: number): string {\n if (text.length === 0) {return '';}\n\n let sanitized = removeControlCharacters(text);\n sanitized = sanitized.trim();\n\n const hasValidMaxLength = typeof maxLength === 'number' && maxLength > 0;\n const exceedsMaxLength = hasValidMaxLength && sanitized.length > maxLength;\n if (exceedsMaxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n return sanitized;\n}\n\n/**\n * Validates and sanitizes a URL to prevent javascript: and data: protocol attacks.\n *\n * @param url - The URL to validate\n * @returns The sanitized URL or empty string if invalid\n */\nexport function sanitizeUrl(url: string): string {\n if (url.length === 0) {return '';}\n\n const trimmedLower = url.trim().toLowerCase();\n const protocol = trimmedLower.split(':')[0];\n\n // Block dangerous protocols\n const isDangerousProtocol = protocol === 'javascript' || protocol === 'data' || protocol === 'vbscript';\n if (isDangerousProtocol) \n {return '';}\n \n\n return url.trim();\n}\n","/**\n * Framework-agnostic redirect escape hatch.\n *\n * WHY THIS EXISTS: non-React code (an axios 401 interceptor, an auth provider's\n * token-refresh failure path) needs to navigate, but has no access to a router\n * hook. The app registers its router's push/replace once via `setRedirectHandler`;\n * everything else calls `redirectTo` and does not care which router is mounted.\n *\n * Three layers of fallback, in order:\n * 1. the registered handler (the app's router),\n * 2. `window.location.replace` if no handler is registered yet,\n * 3. a queue, drained on the next `setRedirectHandler` call — so a redirect fired\n * before the router mounts is not silently dropped.\n *\n * Layer 1 additionally schedules a watchdog: some routers accept a `push` for a\n * route they cannot resolve and leave the URL untouched, which strands the user on\n * the page they were supposed to leave. If the location has not changed after\n * `NAVIGATION_CHECK_DELAY_MS`, a hard `window.location.replace` is forced.\n *\n * Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and\n * katalogos-web. Pure DOM + timers — no React, no router, no framework import.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020`. Adding DOM globally would let every\n * other util in the package reach for browser globals that are absent at runtime\n * on the server. Declaring the ~4 properties used here keeps that door shut.\n */\ninterface BrowserWindow {\n location: {\n pathname: string;\n search: string;\n hash: string;\n replace: (url: string) => void;\n };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** Delay in ms before checking whether the registered handler actually navigated. */\nconst NAVIGATION_CHECK_DELAY_MS = 150;\n\nlet redirectHandler: ((path: string) => void) | null = null;\nconst queuedRedirects: string[] = [];\n\n/**\n * Strips Expo-Router group segments — `/(protected)/settings` → `/settings`.\n * Group folders are a routing-config device and never appear in a real URL, so a\n * raw group path handed to `window.location` would 404.\n */\nfunction normalizeRouterPath(path: string): string {\n const normalized = path.replace(/\\/\\([^/]+\\)/g, '');\n return normalized === '' ? '/' : normalized;\n}\n\n/** Current location as path+search+hash, or null outside a browser (SSR/native). */\nfunction getCurrentLocation(): string | null {\n if (typeof window === 'undefined') {return null;}\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\n/** Force a hard navigation if the registered handler left the location unchanged. */\nfunction scheduleFallbackNavigation(before: string, normalized: string): void {\n setTimeout(() => {\n try {\n const after = getCurrentLocation();\n if (after === before && typeof window !== 'undefined') {window.location.replace(normalized);}\n } catch {\n /* ignore — window may have gone away (unmount/teardown) */\n }\n }, NAVIGATION_CHECK_DELAY_MS);\n}\n\n/** Attempt a hard browser navigation. Returns false outside a browser or on throw. */\nfunction tryWindowNavigation(normalized: string): boolean {\n if (typeof window === 'undefined') {return false;}\n try {\n window.location.replace(normalized);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Register the app's router navigate function and drain any redirects that were\n * queued before it mounted. Call once, from the root layout.\n */\nexport function setRedirectHandler(h: (path: string) => void): void {\n redirectHandler = h;\n while (queuedRedirects.length > 0) {\n const p = queuedRedirects.shift();\n try {\n if (typeof p === 'string' && p.length > 0) {h(normalizeRouterPath(p));}\n } catch {\n /* swallow; the handler may still not be ready */\n }\n }\n}\n\n/**\n * Navigate to `path` from anywhere, including outside React.\n * Safe to call before the router mounts — the redirect is queued and replayed.\n */\nexport function redirectTo(path: string): void {\n const normalized = normalizeRouterPath(path);\n if (redirectHandler) {\n const before = getCurrentLocation();\n try {\n redirectHandler(normalized);\n if (typeof before === 'string') {scheduleFallbackNavigation(before, normalized);}\n return;\n } catch {\n /* fall through to a hard navigation */\n }\n }\n if (tryWindowNavigation(normalized)) {return;}\n queuedRedirects.push(normalized);\n}\n\n/**\n * Clear the registered handler and any queued redirects.\n * Exists for tests — module state otherwise leaks between cases.\n */\nexport function resetRedirectHandler(): void {\n redirectHandler = null;\n queuedRedirects.length = 0;\n}\n","/**\n * Detection + one-shot auto-recovery for stale-chunk load failures.\n *\n * WHY (UX Move 3, would have masked P0-01): after a deploy, the browser may hold\n * a stale `index.html` that references hashed JS chunks which now 404 — surfacing\n * as a `ChunkLoadError` / \"failed to fetch dynamically imported module\". A single\n * guarded `location.reload()` re-fetches the fresh entry + chunks and the app\n * recovers with no user-visible error. The sessionStorage one-shot flag prevents\n * a reload LOOP if the reload does not fix it (then the boundary shows a manual\n * Reload action instead).\n *\n * Pure + port-injectable: detection is a pure predicate; the storage + reload\n * side effects are ports (default to `window`) so unit tests run deterministically\n * in jsdom without a real navigation.\n *\n * Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in\n * kefi-web, erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd).\n * The original file's own header predicted this move. Pairs with\n * `<AppErrorBoundary>` from `@dloizides/ui-feedback`, which consumes these as its\n * `retryable` / `onMount` / `recover` injections.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020` (see `navigation/redirect.ts`, which\n * makes the same call). Declaring the few properties used here keeps every other\n * util in the package from reaching for browser globals absent on the server.\n */\ninterface ChunkRecoveryStorage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n removeItem: (key: string) => void;\n}\n\ninterface BrowserWindow {\n sessionStorage: ChunkRecoveryStorage;\n location: { reload: () => void };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** sessionStorage key for the one-shot auto-reload guard. */\nconst CHUNK_RELOAD_FLAG = 'ui.chunkReload.attempted';\n\n/** Messages/names emitted by webpack/metro/vite/Safari for a stale-chunk failure. */\nconst CHUNK_ERROR_PATTERNS: readonly RegExp[] = [\n /ChunkLoadError/i,\n /Loading chunk [\\w-]+ failed/i,\n /Loading CSS chunk/i,\n /Failed to fetch dynamically imported module/i,\n /error loading dynamically imported module/i,\n /Importing a module script failed/i,\n /is not a valid JavaScript MIME type/i,\n];\n\n/** Injectable side-effect ports; default to the real `window`. */\nexport interface ChunkRecoveryPorts {\n hasFlag: () => boolean;\n setFlag: () => void;\n clearFlag: () => void;\n reload: () => void;\n}\n\n/** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */\nexport function isChunkLoadError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if (error.name === 'ChunkLoadError') {\n return true;\n }\n const haystack = `${error.name} ${error.message}`;\n return CHUNK_ERROR_PATTERNS.some((pattern) => pattern.test(haystack));\n}\n\nfunction resolveStorage(): ChunkRecoveryStorage | undefined {\n if (typeof window === 'undefined') {\n return undefined;\n }\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\nfunction defaultPorts(): ChunkRecoveryPorts {\n const storage = resolveStorage();\n const hasWindow = typeof window !== 'undefined';\n return {\n hasFlag: () => Boolean(storage?.getItem(CHUNK_RELOAD_FLAG)),\n setFlag: (): void => {\n storage?.setItem(CHUNK_RELOAD_FLAG, '1');\n },\n clearFlag: (): void => {\n storage?.removeItem(CHUNK_RELOAD_FLAG);\n },\n reload: (): void => {\n if (hasWindow) {\n window.location.reload();\n }\n },\n };\n}\n\n/**\n * Attempt a one-shot reload to recover from a stale-chunk error.\n * Returns `true` when a reload was triggered, `false` when the one-shot guard was\n * already spent (the caller then shows a manual Reload action, never a loop).\n */\nexport function attemptChunkRecovery(ports: ChunkRecoveryPorts = defaultPorts()): boolean {\n if (ports.hasFlag()) {\n return false;\n }\n ports.setFlag();\n ports.reload();\n return true;\n}\n\n/** Clear the one-shot guard after a clean load so a FUTURE deploy can auto-recover. */\nexport function clearChunkRecoveryFlag(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.clearFlag();\n}\n\n/** Manually reload the page (the Reload action). */\nexport function reloadPage(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.reload();\n}\n","export { isValueDefined } from './isValueDefined';\r\nexport { isNotEmptyArray } from './isNotEmptyArray';\r\nexport { isNotEmptyString } from './isNotEmptyString';\r\nexport { isNullOrUndefined } from './isNullOrUndefined';\r\nexport { isEmptyArray } from './isEmptyArray';\r\nexport { isEmptyString } from './isEmptyString';\r\n","export { assertDefined } from './assertDefined';\r\n","export { getErrorMessage } from './getErrorMessage';\n","export { formatDate } from './formatDate';\n","export { toCsv } from './toCsv';\nexport type { CsvColumn } from './toCsv';\n","export { setRedirectHandler, redirectTo, resetRedirectHandler } from './redirect';\n","export {\n isChunkLoadError,\n attemptChunkRecovery,\n clearChunkRecoveryFlag,\n reloadPage,\n} from './chunkRecovery';\nexport type { ChunkRecoveryPorts } from './chunkRecovery';\n","export {\n sanitizeHtml,\n removeControlCharacters,\n sanitizeNotificationMessage,\n sanitizeText,\n sanitizeUrl,\n} from './sanitize';\n"]}
1
+ {"version":3,"sources":["../src/guards/isValueDefined.ts","../src/guards/isNotEmptyArray.ts","../src/guards/isNotEmptyString.ts","../src/guards/isNullOrUndefined.ts","../src/guards/isEmptyArray.ts","../src/guards/isEmptyString.ts","../src/assertions/assertDefined.ts","../src/async/fireAndForget.ts","../src/error/getErrorMessage.ts","../src/format/formatDate.ts","../src/csv/toCsv.ts","../src/sanitize/sanitize.ts","../src/navigation/redirect.ts","../src/chunkRecovery/chunkRecovery.ts","../src/guards/index.ts","../src/assertions/index.ts","../src/async/index.ts","../src/error/index.ts","../src/format/index.ts","../src/csv/index.ts","../src/navigation/index.ts","../src/chunkRecovery/index.ts","../src/sanitize/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\r\n * Type guard that checks if a value is neither null nor undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is defined (not null and not undefined)\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isValueDefined(value)) {\r\n * // value is narrowed to string\r\n * console.log(value.length);\r\n * }\r\n * ```\r\n */\r\nexport function isValueDefined<T>(value: T | null | undefined): value is T {\r\n return value !== undefined && value !== null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is a non-empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with at least one element\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] | undefined = getItems();\r\n * if (isNotEmptyArray(items)) {\r\n * // items is narrowed to string[] with at least one element\r\n * console.log(items[0]); // Safe to access\r\n * }\r\n * ```\r\n */\r\nexport function isNotEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: number } {\r\n return Array.isArray(value) && value.length > 0;\r\n}\r\n","/**\r\n * Checks if a value is a non-empty string (after trimming whitespace).\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is a string with non-whitespace content\r\n *\r\n * @example\r\n * ```typescript\r\n * isNotEmptyString('hello'); // true\r\n * isNotEmptyString(' '); // false\r\n * isNotEmptyString(''); // false\r\n * isNotEmptyString(undefined); // false\r\n * ```\r\n */\r\nexport function isNotEmptyString(value: string | null | undefined): value is string {\r\n return typeof value === 'string' && value.trim().length > 0;\r\n}\r\n","/**\r\n * Type guard that checks if a value is null or undefined.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const value: string | null | undefined = getValue();\r\n * if (isNullOrUndefined(value)) {\r\n * // value is narrowed to null | undefined\r\n * console.log('Value is not set');\r\n * }\r\n * ```\r\n */\r\nexport function isNullOrUndefined(value: unknown): value is null | undefined {\r\n return value === undefined || value === null;\r\n}\r\n","/**\r\n * Type guard that checks if a value is an empty array.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an array with zero elements\r\n *\r\n * @example\r\n * ```typescript\r\n * const items: string[] = getItems();\r\n * if (isEmptyArray(items)) {\r\n * console.log('No items found');\r\n * }\r\n * ```\r\n */\r\nexport function isEmptyArray<T>(\r\n value: readonly T[] | null | undefined\r\n): value is readonly T[] & { readonly length: 0 } {\r\n return Array.isArray(value) && value.length === 0;\r\n}\r\n","/**\r\n * Checks if a value is an empty string or contains only whitespace.\r\n *\r\n * @param value - The value to check\r\n * @returns True if the value is an empty string or whitespace-only\r\n *\r\n * @example\r\n * ```typescript\r\n * isEmptyString(''); // true\r\n * isEmptyString(' '); // true\r\n * isEmptyString('hello'); // false\r\n * isEmptyString(undefined); // false (not a string)\r\n * isEmptyString(null); // false (not a string)\r\n * ```\r\n */\r\nexport function isEmptyString(value: string | null | undefined): boolean {\r\n return typeof value === 'string' && value.trim().length === 0;\r\n}\r\n","/**\r\n * Asserts that a value is defined (not null or undefined).\r\n * Throws an error if the value is null or undefined.\r\n *\r\n * @param value - The value to assert\r\n * @param message - Optional error message\r\n * @throws Error if value is null or undefined\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = getUser(); // User | null\r\n * assertDefined(user, 'User must exist');\r\n * // user is now narrowed to User\r\n * console.log(user.name);\r\n * ```\r\n */\r\nexport function assertDefined<T>(\r\n value: T | null | undefined,\r\n message?: string\r\n): asserts value is T {\r\n if (value === undefined || value === null) {\r\n throw new Error(message ?? 'Value must be defined');\r\n }\r\n}\r\n","/** Starts a promise without awaiting it and swallows its rejection. */\nexport function fireAndForget(promise: Promise<unknown>): void {\n promise.catch(() => undefined);\n}\n","import { isValueDefined } from '../guards/isValueDefined';\n\ninterface ErrorWithMessage {\n message?: unknown;\n}\n\nfunction isErrorWithMessage(value: unknown): value is ErrorWithMessage {\n return typeof value === 'object' && isValueDefined(value) && 'message' in value;\n}\n\n/**\n * Extracts a human-readable error message from various error shapes\n * (Error, string, or an object with a `message` property).\n *\n * @param value - The error value.\n * @param fallback - Returned when no message can be extracted (default: 'Unknown error').\n */\nexport function getErrorMessage(value: unknown, fallback = 'Unknown error'): string {\n if (value instanceof Error) {return value.message;}\n if (typeof value === 'string') {return value;}\n if (isErrorWithMessage(value)) {\n const message = value.message;\n if (typeof message === 'string' && message.length > 0) {return message;}\n }\n return fallback;\n}\n","/**\n * Locale-aware date formatting (pure — the caller supplies the locale, e.g. from i18n).\n * Returns '' for null/undefined; falls back to the ISO date if Intl formatting throws.\n *\n * @example formatDate(new Date(), 'en') // \"6/15/2024\"\n * @example formatDate(new Date(), 'en', { year: 'numeric', month: 'long' }) // \"June 2024\"\n * @example formatDate(null, 'en') // \"\"\n */\nexport function formatDate(\n date: Date | null | undefined,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n if (date === null || date === undefined) {return '';}\n\n const resolvedLocale = locale !== '' ? locale : 'en';\n\n try {\n return new Intl.DateTimeFormat(resolvedLocale, options).format(date);\n } catch {\n // Fallback to ISO date if formatting fails (invalid locale/options).\n return date.toISOString().split('T')[0] ?? '';\n }\n}\n","/**\n * RFC 4180 CSV serialization — the pure, DOM-free core of \"export the rows on screen to CSV\".\n *\n * The escaping is the whole risk surface: a comma, a quote or a newline inside a value (a\n * counterparty name, an address) is exactly what turns one row into two columns or two rows in a\n * spreadsheet. So the serializer lives here as a pure, total function a unit test can hammer, split\n * from any browser download shell (which is a side-effecting, app-owned concern).\n *\n * Promoted from finreg-web's `utils/exportCsv.ts` (ZY-25) — the escaping logic is universal across\n * the fleet; the DOM `<a download>` shell that consumes it stays in the app until a 2nd consumer.\n */\nimport { isValueDefined } from '../guards/isValueDefined';\n\n/** One column of a CSV export: a (caller-localized) header and how to read the cell from a row. */\nexport interface CsvColumn<T> {\n /** The column header — already localized by the caller. */\n header: string;\n /** Reads the cell value from a row. `null`/`undefined` become an empty cell. */\n value: (row: T) => string | number | null | undefined;\n}\n\n/** RFC 4180: a field must be quoted when it contains a quote, a comma, or a line break. */\nconst NEEDS_QUOTING = /[\"',\\r\\n]/;\n\n// A more permissive test than strictly required (it also quotes apostrophes), so a value like\n// O'Brien is never split by a spreadsheet that treats a leading quote specially — harmless, and it\n// keeps the rule to one regex.\nconst QUOTE = '\"';\nconst ESCAPED_QUOTE = '\"\"';\n/** Every embedded quote, so each is doubled per RFC 4180 (ES2020-safe; no `replaceAll`). */\nconst EVERY_QUOTE = /\"/g;\n\n/** RFC 4180 uses CRLF between records; Excel and Sheets both require it. */\nconst ROW_SEPARATOR = '\\r\\n';\nconst FIELD_SEPARATOR = ',';\n\n/** Quote and escape a single field per RFC 4180. `null`/`undefined` → an empty field. */\nfunction escapeField(raw: string | number | null | undefined): string {\n const text = isValueDefined(raw) ? String(raw) : '';\n if (!NEEDS_QUOTING.test(text)) {\n return text;\n }\n return `${QUOTE}${text.replace(EVERY_QUOTE, ESCAPED_QUOTE)}${QUOTE}`;\n}\n\n/**\n * Serialize rows to an RFC 4180 CSV string — a header row followed by one row per item.\n *\n * Pure and total: an empty `rows` yields the header line alone, which opens as an empty-but-labelled\n * spreadsheet rather than a zero-byte file the operator cannot tell apart from a failure.\n */\nexport function toCsv<T>(rows: readonly T[], columns: ReadonlyArray<CsvColumn<T>>): string {\n const headerLine = columns.map((column) => escapeField(column.header)).join(FIELD_SEPARATOR);\n\n const dataLines = rows.map((row) =>\n columns.map((column) => escapeField(column.value(row))).join(FIELD_SEPARATOR),\n );\n\n return [headerLine, ...dataLines].join(ROW_SEPARATOR);\n}\n","/**\n * Text sanitization utilities for preventing XSS and ensuring safe display.\n * These utilities sanitize user-provided text before rendering.\n */\n\n/**\n * HTML entities that should be escaped to prevent XSS attacks.\n */\nconst HTML_ENTITIES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#x27;',\n '/': '&#x2F;',\n '`': '&#x60;',\n '=': '&#x3D;',\n};\n\n/**\n * Regular expression matching characters that need escaping.\n */\nconst HTML_ESCAPE_REGEX = /[&<>\"'`=/]/g;\n\n/**\n * Unicode code point ranges for dangerous characters.\n * These are control characters and zero-width characters that can be\n * used in homograph attacks or to hide malicious content.\n */\n\n/** C0 control characters range start (NULL) */\nconst C0_CONTROL_START = 0x0000;\n/** C0 control characters range end (Unit Separator) */\nconst C0_CONTROL_END = 0x001f;\n/** C1 control characters range start (DEL and beyond) */\nconst C1_CONTROL_START = 0x007f;\n/** C1 control characters range end */\nconst C1_CONTROL_END = 0x009f;\n/** Zero-width characters range start (Zero Width Space) */\nconst ZERO_WIDTH_START = 0x200b;\n/** Zero-width characters range end (Right-to-Left Mark) */\nconst ZERO_WIDTH_END = 0x200f;\n/** Format characters range start (Line Separator) */\nconst FORMAT_CHARS_START = 0x2028;\n/** Format characters range end (Narrow No-Break Space) */\nconst FORMAT_CHARS_END = 0x202f;\n/** Byte Order Mark code point */\nconst BYTE_ORDER_MARK = 0xfeff;\n\n/**\n * Maximum reasonable length for notification messages.\n */\nconst MAX_NOTIFICATION_LENGTH = 500;\n\n/**\n * Length of ellipsis suffix (\"...\") used when truncating text.\n */\nconst ELLIPSIS_LENGTH = 3;\n\n/**\n * Escapes HTML special characters in a string to prevent XSS attacks.\n * Use this when displaying user-provided content in the UI.\n *\n * @param text - The text to sanitize\n * @returns The sanitized text with HTML entities escaped\n *\n * @example\n * sanitizeHtml('<script>alert(\"xss\")</script>')\n * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'\n */\nexport function sanitizeHtml(text: string): string {\n if (text.length === 0) {return '';}\n return text.replace(HTML_ESCAPE_REGEX, (char) => HTML_ENTITIES[char] ?? char);\n}\n\n/**\n * Control characters and zero-width characters that should be removed.\n * These can be used in homograph attacks or to hide malicious content.\n */\nfunction isDangerousCharCode(code: number): boolean {\n const isC0Control = code >= C0_CONTROL_START && code <= C0_CONTROL_END;\n const isC1Control = code >= C1_CONTROL_START && code <= C1_CONTROL_END;\n const isZeroWidth = code >= ZERO_WIDTH_START && code <= ZERO_WIDTH_END;\n const isFormatChar = code >= FORMAT_CHARS_START && code <= FORMAT_CHARS_END;\n const isBom = code === BYTE_ORDER_MARK;\n return isC0Control || isC1Control || isZeroWidth || isFormatChar || isBom;\n}\n\n/**\n * Removes control characters and zero-width characters from text.\n * These characters can be used to create misleading text or hide content.\n *\n * @param text - The text to sanitize\n * @returns The text with dangerous characters removed\n */\nexport function removeControlCharacters(text: string): string {\n if (text.length === 0) {return '';}\n let out = '';\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (!isDangerousCharCode(code)) {out += text[i];}\n }\n return out;\n}\n\n/**\n * Sanitizes a notification message for safe display.\n * This function:\n * 1. Removes control/zero-width characters\n * 2. Trims whitespace\n * 3. Truncates to a reasonable length\n * 4. Escapes HTML entities (for web platform)\n *\n * Note: React Native's Text component already escapes HTML by default,\n * but this provides defense-in-depth and handles the web platform.\n *\n * @param message - The notification message to sanitize\n * @param maxLength - Maximum length (default: 500)\n * @returns The sanitized message\n *\n * @example\n * sanitizeNotificationMessage(' Hello <b>World</b> ')\n * // Returns: 'Hello &lt;b&gt;World&lt;/b&gt;'\n */\nexport function sanitizeNotificationMessage(\n message: string,\n maxLength: number = MAX_NOTIFICATION_LENGTH\n): string {\n if (message.length === 0) {return '';}\n\n // Remove dangerous characters first\n let sanitized = removeControlCharacters(message);\n\n // Trim whitespace\n sanitized = sanitized.trim();\n\n // Truncate if too long\n if (sanitized.length > maxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n // Escape HTML entities\n sanitized = sanitizeHtml(sanitized);\n\n return sanitized;\n}\n\n/**\n * Sanitizes text for safe display, without HTML escaping.\n * Use this for React Native components where HTML escaping isn't needed\n * but control character removal and length limits are still important.\n *\n * @param text - The text to sanitize\n * @param maxLength - Maximum length (optional)\n * @returns The sanitized text\n */\nexport function sanitizeText(text: string, maxLength?: number): string {\n if (text.length === 0) {return '';}\n\n let sanitized = removeControlCharacters(text);\n sanitized = sanitized.trim();\n\n const hasValidMaxLength = typeof maxLength === 'number' && maxLength > 0;\n const exceedsMaxLength = hasValidMaxLength && sanitized.length > maxLength;\n if (exceedsMaxLength) \n {sanitized = `${sanitized.substring(0, maxLength - ELLIPSIS_LENGTH)}...`;}\n \n\n return sanitized;\n}\n\n/**\n * Validates and sanitizes a URL to prevent javascript: and data: protocol attacks.\n *\n * @param url - The URL to validate\n * @returns The sanitized URL or empty string if invalid\n */\nexport function sanitizeUrl(url: string): string {\n if (url.length === 0) {return '';}\n\n const trimmedLower = url.trim().toLowerCase();\n const protocol = trimmedLower.split(':')[0];\n\n // Block dangerous protocols\n const isDangerousProtocol = protocol === 'javascript' || protocol === 'data' || protocol === 'vbscript';\n if (isDangerousProtocol) \n {return '';}\n \n\n return url.trim();\n}\n","/**\n * Framework-agnostic redirect escape hatch.\n *\n * WHY THIS EXISTS: non-React code (an axios 401 interceptor, an auth provider's\n * token-refresh failure path) needs to navigate, but has no access to a router\n * hook. The app registers its router's push/replace once via `setRedirectHandler`;\n * everything else calls `redirectTo` and does not care which router is mounted.\n *\n * Three layers of fallback, in order:\n * 1. the registered handler (the app's router),\n * 2. `window.location.replace` if no handler is registered yet,\n * 3. a queue, drained on the next `setRedirectHandler` call — so a redirect fired\n * before the router mounts is not silently dropped.\n *\n * Layer 1 additionally schedules a watchdog: some routers accept a `push` for a\n * route they cannot resolve and leave the URL untouched, which strands the user on\n * the page they were supposed to leave. If the location has not changed after\n * `NAVIGATION_CHECK_DELAY_MS`, a hard `window.location.replace` is forced.\n *\n * Promoted from the byte-identical `lib/navigation.ts` twins in erevna-web and\n * katalogos-web. Pure DOM + timers — no React, no router, no framework import.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020`. Adding DOM globally would let every\n * other util in the package reach for browser globals that are absent at runtime\n * on the server. Declaring the ~4 properties used here keeps that door shut.\n */\ninterface BrowserWindow {\n location: {\n pathname: string;\n search: string;\n hash: string;\n replace: (url: string) => void;\n };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/** Delay in ms before checking whether the registered handler actually navigated. */\nconst NAVIGATION_CHECK_DELAY_MS = 150;\n\nlet redirectHandler: ((path: string) => void) | null = null;\nconst queuedRedirects: string[] = [];\n\n/**\n * Strips Expo-Router group segments — `/(protected)/settings` → `/settings`.\n * Group folders are a routing-config device and never appear in a real URL, so a\n * raw group path handed to `window.location` would 404.\n */\nfunction normalizeRouterPath(path: string): string {\n const normalized = path.replace(/\\/\\([^/]+\\)/g, '');\n return normalized === '' ? '/' : normalized;\n}\n\n/** Current location as path+search+hash, or null outside a browser (SSR/native). */\nfunction getCurrentLocation(): string | null {\n if (typeof window === 'undefined') {return null;}\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\n/** Force a hard navigation if the registered handler left the location unchanged. */\nfunction scheduleFallbackNavigation(before: string, normalized: string): void {\n setTimeout(() => {\n try {\n const after = getCurrentLocation();\n if (after === before && typeof window !== 'undefined') {window.location.replace(normalized);}\n } catch {\n /* ignore — window may have gone away (unmount/teardown) */\n }\n }, NAVIGATION_CHECK_DELAY_MS);\n}\n\n/** Attempt a hard browser navigation. Returns false outside a browser or on throw. */\nfunction tryWindowNavigation(normalized: string): boolean {\n if (typeof window === 'undefined') {return false;}\n try {\n window.location.replace(normalized);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Register the app's router navigate function and drain any redirects that were\n * queued before it mounted. Call once, from the root layout.\n */\nexport function setRedirectHandler(h: (path: string) => void): void {\n redirectHandler = h;\n while (queuedRedirects.length > 0) {\n const p = queuedRedirects.shift();\n try {\n if (typeof p === 'string' && p.length > 0) {h(normalizeRouterPath(p));}\n } catch {\n /* swallow; the handler may still not be ready */\n }\n }\n}\n\n/**\n * Navigate to `path` from anywhere, including outside React.\n * Safe to call before the router mounts — the redirect is queued and replayed.\n */\nexport function redirectTo(path: string): void {\n const normalized = normalizeRouterPath(path);\n if (redirectHandler) {\n const before = getCurrentLocation();\n try {\n redirectHandler(normalized);\n if (typeof before === 'string') {scheduleFallbackNavigation(before, normalized);}\n return;\n } catch {\n /* fall through to a hard navigation */\n }\n }\n if (tryWindowNavigation(normalized)) {return;}\n queuedRedirects.push(normalized);\n}\n\n/**\n * Clear the registered handler and any queued redirects.\n * Exists for tests — module state otherwise leaks between cases.\n */\nexport function resetRedirectHandler(): void {\n redirectHandler = null;\n queuedRedirects.length = 0;\n}\n","/**\n * Detection + one-shot auto-recovery for stale-chunk load failures.\n *\n * WHY (UX Move 3, would have masked P0-01): after a deploy, the browser may hold\n * a stale `index.html` that references hashed JS chunks which now 404 — surfacing\n * as a `ChunkLoadError` / \"failed to fetch dynamically imported module\". A single\n * guarded `location.reload()` re-fetches the fresh entry + chunks and the app\n * recovers with no user-visible error. The sessionStorage one-shot flag prevents\n * a reload LOOP if the reload does not fix it (then the boundary shows a manual\n * Reload action instead).\n *\n * Pure + port-injectable: detection is a pure predicate; the storage + reload\n * side effects are ports (default to `window`) so unit tests run deterministically\n * in jsdom without a real navigation.\n *\n * Promoted from the byte-identical `src/utils/chunkLoadRecovery.ts` triplets in\n * kefi-web, erevna-web and katalogos-web (extract-on-2nd-use; this was the 3rd).\n * The original file's own header predicted this move. Pairs with\n * `<AppErrorBoundary>` from `@dloizides/ui-feedback`, which consumes these as its\n * `retryable` / `onMount` / `recover` injections.\n */\n\n/**\n * The slice of `window` this module touches, declared structurally.\n *\n * WHY NOT `lib: [\"DOM\"]`: `@dloizides/utils` ships to Node consumers too, and its\n * tsconfig deliberately lists only `ES2020` (see `navigation/redirect.ts`, which\n * makes the same call). Declaring the few properties used here keeps every other\n * util in the package from reaching for browser globals absent on the server.\n */\ninterface ChunkRecoveryStorage {\n getItem: (key: string) => string | null;\n setItem: (key: string, value: string) => void;\n removeItem: (key: string) => void;\n}\n\ninterface BrowserWindow {\n sessionStorage: ChunkRecoveryStorage;\n location: { reload: () => void };\n}\n\ndeclare const window: BrowserWindow | undefined;\n\n/**\n * sessionStorage key holding the epoch-ms timestamp of the last auto-reload attempt.\n *\n * A TIMESTAMP, not the old boolean `ui.chunkReload.attempted`. The boolean was released\n * by the consumer on a clean mount, and a lazy route makes the mount clean: the root\n * boundary commits (a Suspense fallback renders), `componentDidMount` releases the guard,\n * and only THEN does the dynamic import reject with a 404 chunk. Recovery re-armed itself\n * on every pass, so the reload loop the guard exists to bound ran unbounded.\n */\nconst CHUNK_RELOAD_AT = 'ui.chunkReload.attemptedAt';\n\n/**\n * How long one recorded attempt keeps blocking a second automatic reload.\n *\n * WHY A WINDOW RATHER THAN A COUNTER: attempt-counting bounds the loop too, but a counter\n * has to be reset by somebody, and the only place a consumer can put that reset is a clean\n * mount — the exact release that caused this defect. A window expires by itself, so there is\n * no release call to fire at the wrong moment. One minute is far longer than a reload-plus-boot\n * cycle (single-digit seconds, which is what the loop runs at) and far shorter than the gap to\n * a genuinely later, unrelated rollout, which is still allowed to auto-recover once.\n */\nconst RECOVERY_COOLDOWN_MS = 60_000;\n\n/** Messages/names emitted by webpack/metro/vite/Safari for a stale-chunk failure. */\nconst CHUNK_ERROR_PATTERNS: readonly RegExp[] = [\n /ChunkLoadError/i,\n /Loading chunk [\\w-]+ failed/i,\n /Loading CSS chunk/i,\n /Failed to fetch dynamically imported module/i,\n /error loading dynamically imported module/i,\n /Importing a module script failed/i,\n /is not a valid JavaScript MIME type/i,\n];\n\n/** Injectable side-effect ports; default to the real `window`. */\nexport interface ChunkRecoveryPorts {\n /** Epoch-ms of the last recorded attempt, or `null` when none is stored. */\n readAttemptAt: () => number | null;\n recordAttemptAt: (at: number) => void;\n clearAttempt: () => void;\n reload: () => void;\n now: () => number;\n}\n\n/** True when `error` looks like a stale-chunk / failed-dynamic-import failure. */\nexport function isChunkLoadError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if (error.name === 'ChunkLoadError') {\n return true;\n }\n const haystack = `${error.name} ${error.message}`;\n return CHUNK_ERROR_PATTERNS.some((pattern) => pattern.test(haystack));\n}\n\nfunction resolveStorage(): ChunkRecoveryStorage | undefined {\n if (typeof window === 'undefined') {\n return undefined;\n }\n try {\n return window.sessionStorage;\n } catch {\n return undefined;\n }\n}\n\nfunction defaultPorts(): ChunkRecoveryPorts {\n const storage = resolveStorage();\n const hasWindow = typeof window !== 'undefined';\n return {\n readAttemptAt: (): number | null => {\n const raw = storage?.getItem(CHUNK_RELOAD_AT);\n if (raw === null || raw === undefined || raw === '') {\n return null;\n }\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : null;\n },\n recordAttemptAt: (at: number): void => {\n storage?.setItem(CHUNK_RELOAD_AT, String(at));\n },\n clearAttempt: (): void => {\n storage?.removeItem(CHUNK_RELOAD_AT);\n },\n now: (): number => Date.now(),\n reload: (): void => {\n if (hasWindow) {\n window.location.reload();\n }\n },\n };\n}\n\n/** True when a recorded attempt is still inside the cooldown window. */\nfunction isWithinCooldown(attemptedAt: number | null, now: number): boolean {\n return attemptedAt !== null && now - attemptedAt < RECOVERY_COOLDOWN_MS;\n}\n\n/**\n * Attempt a guarded reload to recover from a stale-chunk error.\n * Returns `true` when a reload was triggered, `false` while the cooldown from the previous\n * attempt is still running (the caller then shows a manual Reload action, never a loop).\n *\n * `recordAttemptAt` and `reload` stay synchronous and adjacent so the record survives the\n * navigation that follows.\n */\nexport function attemptChunkRecovery(ports: ChunkRecoveryPorts = defaultPorts()): boolean {\n const now = ports.now();\n if (isWithinCooldown(ports.readAttemptAt(), now)) {\n return false;\n }\n ports.recordAttemptAt(now);\n ports.reload();\n return true;\n}\n\n/**\n * Release the guard after a clean load — but ONLY once the cooldown has expired.\n *\n * Consumers call this from the root boundary's clean-mount hook, and on a lazy route that\n * mount happens BEFORE the chunk rejects. Refusing to clear a live record is what makes the\n * bound hold no matter when the consumer calls this: an expired record permits recovery\n * anyway, so clearing it only tidies storage, while a live one is left alone.\n */\nexport function clearChunkRecoveryFlag(ports: ChunkRecoveryPorts = defaultPorts()): void {\n if (isWithinCooldown(ports.readAttemptAt(), ports.now())) {\n return;\n }\n ports.clearAttempt();\n}\n\n/** Manually reload the page (the Reload action). */\nexport function reloadPage(ports: ChunkRecoveryPorts = defaultPorts()): void {\n ports.reload();\n}\n","export { isValueDefined } from './isValueDefined';\r\nexport { isNotEmptyArray } from './isNotEmptyArray';\r\nexport { isNotEmptyString } from './isNotEmptyString';\r\nexport { isNullOrUndefined } from './isNullOrUndefined';\r\nexport { isEmptyArray } from './isEmptyArray';\r\nexport { isEmptyString } from './isEmptyString';\r\n","export { assertDefined } from './assertDefined';\r\n","export { fireAndForget } from './fireAndForget';\n","export { getErrorMessage } from './getErrorMessage';\n","export { formatDate } from './formatDate';\n","export { toCsv } from './toCsv';\nexport type { CsvColumn } from './toCsv';\n","export { setRedirectHandler, redirectTo, resetRedirectHandler } from './redirect';\n","export {\n isChunkLoadError,\n attemptChunkRecovery,\n clearChunkRecoveryFlag,\n reloadPage,\n} from './chunkRecovery';\nexport type { ChunkRecoveryPorts } from './chunkRecovery';\n","export {\n sanitizeHtml,\n removeControlCharacters,\n sanitizeNotificationMessage,\n sanitizeText,\n sanitizeUrl,\n} from './sanitize';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dloizides/utils",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Type-safe utility functions for TypeScript projects",
5
5
  "keywords": [
6
6
  "typescript",
@@ -86,7 +86,7 @@
86
86
  "security:audit": "npm audit --audit-level=high",
87
87
  "deps:outdated": "npm outdated || exit 0",
88
88
  "deps:unused": "npx depcheck --ignores \"@types/jest,@types/node,rimraf,size-limit,@size-limit/preset-small-lib\"",
89
- "deps:licenses": "npx license-checker --onlyAllow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;Unlicense;CC0-1.0\"",
89
+ "deps:licenses": "npx license-checker --production --onlyAllow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;Unlicense;CC0-1.0;CC-BY-3.0;CC-BY-4.0;Python-2.0;BlueOak-1.0.0\" && npx license-checker --development --onlyAllow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;Unlicense;CC0-1.0;CC-BY-3.0;CC-BY-4.0;Python-2.0;BlueOak-1.0.0;LGPL-3.0;LGPL-3.0-only;LGPL-3.0-or-later\"",
90
90
  "deps:health": "npm run deps:outdated && npm run deps:unused",
91
91
  "prepublishOnly": "npm run clean && npm run build && npm run test",
92
92
  "postversion": "git push && git push --tags"