@reykjavik/webtools 0.2.1 → 0.2.3

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
@@ -4,11 +4,29 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.2.3
8
+
9
+ _2025-03-31_
10
+
11
+ - `@reykjavik/webtools/errorhandling`:
12
+ - feat: Add a `.mapTo(fn)` method to `ResultTupleObj`s
13
+ - fix: `Result.map` does not catch errors thrown by the mapping function
14
+ - docs: Fix minor error in README code example
15
+
16
+ ## 0.2.2
17
+
18
+ _2024-12-19_
19
+
20
+ - `@reykjavik/webtools/CookieHubConsent`:
21
+ - feat: Allow passing explicit `undefined` as `accountId` to skip loading
22
+ - fix: Note that `window.cookiehub` is possibly `undefined` (before load)
23
+
7
24
  ## 0.2.1
8
25
 
9
26
  _2024-12-17_
10
27
 
11
- - fix: Make typing of failed ResultTuple/ResultTupleObj less ambiguous
28
+ - `@reykjavik/webtools/errorhandling`:
29
+ - fix: Make typing of failed ResultTuple/ResultTupleObj less ambiguous
12
30
 
13
31
  ## 0.2.0
14
32
 
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { EitherObj } from '@reykjavik/hanna-utils';
3
3
  declare global {
4
4
  interface Window {
5
- cookiehub: CookieHub;
5
+ cookiehub?: CookieHub;
6
6
  }
7
7
  }
8
8
  type CookieHub = {
@@ -180,9 +180,11 @@ export type CookieHubProviderProps = EitherObj<{
180
180
  * extracted from the script embed URL like this:
181
181
  * `"https://cookiehub.net/c2/[ACCOUNT_ID].js"`
182
182
  *
183
+ * Pass `undefined` to disable/skip the script loading
184
+ *
183
185
  * @see https://support.cookiehub.com/article/155-manual-implementation-guide
184
186
  */
185
- accountId: string;
187
+ accountId: string | undefined;
186
188
  }, {
187
189
  /**
188
190
  * The full CookieHub embed script URL.
@@ -65,14 +65,26 @@ const moveCookiehubScriptInDomTree = () => {
65
65
  */
66
66
  const CookieHubProvider = (props) => {
67
67
  const [state, setState] = (0, react_1.useState)(initialConsentState);
68
+ const scriptUrLOrId = props.scriptUrl
69
+ ? `@ ${props.scriptUrl}`
70
+ : props.accountId || undefined;
68
71
  (0, react_1.useEffect)(() => {
72
+ if (!scriptUrLOrId) {
73
+ return;
74
+ }
75
+ const scriptSrc = scriptUrLOrId.startsWith('@ ')
76
+ ? scriptUrLOrId.slice(2)
77
+ : scriptUrlTemplate.replace(idToken, scriptUrLOrId);
78
+ if (window.cookiehub && document.querySelector('script#cookiehub-script')) {
79
+ // We can't load the script from more than one source at a time
80
+ console.warn('CookieHub script already loaded.');
81
+ return;
82
+ }
83
+ const opts = props.options || {};
69
84
  const script = document.createElement('script');
70
85
  script.async = true;
71
- const opts = props.options || {};
72
- script.src =
73
- props.scriptUrl != null
74
- ? props.scriptUrl
75
- : scriptUrlTemplate.replace(idToken, props.accountId);
86
+ script.id = 'cookiehub-script';
87
+ script.src = scriptSrc;
76
88
  script.onload = () => {
77
89
  window.cookiehub.load({
78
90
  ...opts,
@@ -124,8 +136,11 @@ const CookieHubProvider = (props) => {
124
136
  props.onError && (script.onerror = props.onError);
125
137
  document.body.append(script);
126
138
  },
139
+ // Unless we can find a safe way to tear down the CookieHub script and
140
+ // clean up after it we can only load it once, so monitoring anything other
141
+ // than accountId or scriptUrl is pointless.
127
142
  // eslint-disable-next-line react-hooks/exhaustive-deps
128
- []);
143
+ [scriptUrLOrId]);
129
144
  return (react_1.default.createElement(CookieHubContext.Provider, { value: state }, props.children));
130
145
  };
131
146
  exports.CookieHubProvider = CookieHubProvider;
package/README.md CHANGED
@@ -34,6 +34,7 @@ bun add @reykjavik/webtools
34
34
  - [`Result` Singleton](#result-singleton)
35
35
  - [Type `ResultTuple`](#type-resulttuple)
36
36
  - [Type `ResultTupleObj`](#type-resulttupleobj)
37
+ - [Type `ResultTupleObj.mapTo`](#type-resulttupleobjmapto)
37
38
  - [`Result.catch`](#resultcatch)
38
39
  - [`Result.map`](#resultmap)
39
40
  - [`Result.Success`](#resultsuccess)
@@ -456,10 +457,12 @@ if (error) {
456
457
  Discriminated tuple type for a `[error, result]` pair (same as `ResultTuple`)
457
458
  but with named properties `error` and `result` attached for dev convenience.
458
459
 
460
+ It also has a `.mapTo` method ([see below](#type-resulttupleobjmapto)).
461
+
459
462
  ```ts
460
- import { type ResultTuple } from '@reykjavik/webtools/errorhandling';
463
+ import { type ResultTupleObj } from '@reykjavik/webtools/errorhandling';
461
464
 
462
- declare const myResult: ResultTuple<string, Error>;
465
+ declare const myResult: ResultTupleObj<string, Error>;
463
466
 
464
467
  const [error, result] = myResult;
465
468
  // (One of these two is always `undefined`)
@@ -482,6 +485,39 @@ if (myResult.error) {
482
485
  }
483
486
  ```
484
487
 
488
+ #### Type `ResultTupleObj.mapTo`
489
+
490
+ **Syntax:**
491
+ `ResultTupleObj.mapTo<T2, E>(mapResult: (resultValue: T) => T2): ResultTuple<T2, E>`
492
+
493
+ This convenience method allows quick mapping of the `ResultTubleOBj`'s result
494
+ value to a new type. The returned value is also a `ResultTubleOBj`.
495
+
496
+ (Internally this method calls [`Result.map`](#resultmap).)
497
+
498
+ ```ts
499
+ import { type ResultTuple } from '@reykjavik/webtools/errorhandling';
500
+
501
+ declare const myResult: ResultTuple<string, Error>;
502
+
503
+ const mappedResult: ResultTupleObj<number, Error> = myResult.mapTo(
504
+ (result: string) => result.length
505
+ );
506
+
507
+ if (mappedRes.error) {
508
+ console.error(myResult.error.message);
509
+ } else {
510
+ // Here `myResult.result` is a number
511
+ console.log(myResult.result);
512
+ }
513
+ ```
514
+
515
+ If the original `ResultTupleObj` is in a failed state, the mapping function is
516
+ not called.
517
+
518
+ If the mapping function throws an error it gets caught and turned into a
519
+ failed `ResultTupleObj`.
520
+
485
521
  ### `Result.catch`
486
522
 
487
523
  **Syntax:**
@@ -723,8 +759,8 @@ export default function App() {
723
759
  The Component's props have detailed JSDoc comments (displayed in your code
724
760
  editor), but there's a brief summary:
725
761
 
726
- - `accountId?: string` — Your CookieHub account ID. (alternative to
727
- `scriptUrl` prop).
762
+ - `accountId?: string | undefined` — Your CookieHub account ID. (alternative
763
+ to `scriptUrl` prop). Pass `undefined` to skip loading the script.
728
764
  - `scriptUrl?: string` — The full CookieHub embed script URL. (alternative to
729
765
  `accountId` prop).
730
766
  - `options?: CookieHubOptions` — Raw CookieHub options object that gets used
@@ -23,10 +23,12 @@ export declare const asError: (maybeError: unknown) => ErrorFromPayload;
23
23
  type SuccessResult<T> = [error: undefined, result: T] & {
24
24
  error?: undefined;
25
25
  result: T;
26
+ mapTo: <T2, E extends Error = Error>(fn: (result: T) => T2) => ResultTupleObj<T2, E>;
26
27
  };
27
28
  type FailResult<E extends Error> = [error: E, result?: undefined] & {
28
29
  error: E;
29
30
  result?: undefined;
31
+ mapTo: () => FailResult<E>;
30
32
  };
31
33
  /**
32
34
  * Simple bare-bones discriminated tuple type for a [error, result] pair.
package/errorhandling.js CHANGED
@@ -39,11 +39,15 @@ exports.asError = asError;
39
39
  const Success = (result) => {
40
40
  const tuple = [undefined, result];
41
41
  tuple.result = result;
42
+ tuple.mapTo = (fn) =>
43
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
44
+ map(tuple, fn);
42
45
  return tuple;
43
46
  };
44
47
  const Fail = (e) => {
45
48
  const tuple = [(0, exports.asError)(e)];
46
49
  tuple.error = tuple[0];
50
+ tuple.mapTo = () => tuple;
47
51
  return tuple;
48
52
  };
49
53
  function catch_(something) {
@@ -57,6 +61,13 @@ function catch_(something) {
57
61
  return Fail(e);
58
62
  }
59
63
  }
64
+ const map = (result, mapFn) => {
65
+ const [error, resultValue] = result;
66
+ if (error) {
67
+ return Fail(error);
68
+ }
69
+ return catch_(() => mapFn(resultValue));
70
+ };
60
71
  /**
61
72
  * Singleton object with small methods for creating, mapping or handling
62
73
  * `ResultTupleObj` instances.
@@ -90,7 +101,7 @@ exports.Result = {
90
101
  if (error) {
91
102
  return Fail(error);
92
103
  }
93
- return Success(mapFn(resultValue));
104
+ return catch_(() => mapFn(resultValue));
94
105
  },
95
106
  /**
96
107
  * Unwraps a discriminated [error, result] `Result.Tuple`-like object
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { EitherObj } from '@reykjavik/hanna-utils';
3
3
  declare global {
4
4
  interface Window {
5
- cookiehub: CookieHub;
5
+ cookiehub?: CookieHub;
6
6
  }
7
7
  }
8
8
  type CookieHub = {
@@ -180,9 +180,11 @@ export type CookieHubProviderProps = EitherObj<{
180
180
  * extracted from the script embed URL like this:
181
181
  * `"https://cookiehub.net/c2/[ACCOUNT_ID].js"`
182
182
  *
183
+ * Pass `undefined` to disable/skip the script loading
184
+ *
183
185
  * @see https://support.cookiehub.com/article/155-manual-implementation-guide
184
186
  */
185
- accountId: string;
187
+ accountId: string | undefined;
186
188
  }, {
187
189
  /**
188
190
  * The full CookieHub embed script URL.
@@ -39,14 +39,26 @@ const moveCookiehubScriptInDomTree = () => {
39
39
  */
40
40
  export const CookieHubProvider = (props) => {
41
41
  const [state, setState] = useState(initialConsentState);
42
+ const scriptUrLOrId = props.scriptUrl
43
+ ? `@ ${props.scriptUrl}`
44
+ : props.accountId || undefined;
42
45
  useEffect(() => {
46
+ if (!scriptUrLOrId) {
47
+ return;
48
+ }
49
+ const scriptSrc = scriptUrLOrId.startsWith('@ ')
50
+ ? scriptUrLOrId.slice(2)
51
+ : scriptUrlTemplate.replace(idToken, scriptUrLOrId);
52
+ if (window.cookiehub && document.querySelector('script#cookiehub-script')) {
53
+ // We can't load the script from more than one source at a time
54
+ console.warn('CookieHub script already loaded.');
55
+ return;
56
+ }
57
+ const opts = props.options || {};
43
58
  const script = document.createElement('script');
44
59
  script.async = true;
45
- const opts = props.options || {};
46
- script.src =
47
- props.scriptUrl != null
48
- ? props.scriptUrl
49
- : scriptUrlTemplate.replace(idToken, props.accountId);
60
+ script.id = 'cookiehub-script';
61
+ script.src = scriptSrc;
50
62
  script.onload = () => {
51
63
  window.cookiehub.load({
52
64
  ...opts,
@@ -98,8 +110,11 @@ export const CookieHubProvider = (props) => {
98
110
  props.onError && (script.onerror = props.onError);
99
111
  document.body.append(script);
100
112
  },
113
+ // Unless we can find a safe way to tear down the CookieHub script and
114
+ // clean up after it we can only load it once, so monitoring anything other
115
+ // than accountId or scriptUrl is pointless.
101
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
102
- []);
117
+ [scriptUrLOrId]);
103
118
  return (React.createElement(CookieHubContext.Provider, { value: state }, props.children));
104
119
  };
105
120
  // ---------------------------------------------------------------------------
@@ -23,10 +23,12 @@ export declare const asError: (maybeError: unknown) => ErrorFromPayload;
23
23
  type SuccessResult<T> = [error: undefined, result: T] & {
24
24
  error?: undefined;
25
25
  result: T;
26
+ mapTo: <T2, E extends Error = Error>(fn: (result: T) => T2) => ResultTupleObj<T2, E>;
26
27
  };
27
28
  type FailResult<E extends Error> = [error: E, result?: undefined] & {
28
29
  error: E;
29
30
  result?: undefined;
31
+ mapTo: () => FailResult<E>;
30
32
  };
31
33
  /**
32
34
  * Simple bare-bones discriminated tuple type for a [error, result] pair.
@@ -34,11 +34,15 @@ export const asError = (maybeError) => {
34
34
  const Success = (result) => {
35
35
  const tuple = [undefined, result];
36
36
  tuple.result = result;
37
+ tuple.mapTo = (fn) =>
38
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
39
+ map(tuple, fn);
37
40
  return tuple;
38
41
  };
39
42
  const Fail = (e) => {
40
43
  const tuple = [asError(e)];
41
44
  tuple.error = tuple[0];
45
+ tuple.mapTo = () => tuple;
42
46
  return tuple;
43
47
  };
44
48
  function catch_(something) {
@@ -52,6 +56,13 @@ function catch_(something) {
52
56
  return Fail(e);
53
57
  }
54
58
  }
59
+ const map = (result, mapFn) => {
60
+ const [error, resultValue] = result;
61
+ if (error) {
62
+ return Fail(error);
63
+ }
64
+ return catch_(() => mapFn(resultValue));
65
+ };
55
66
  /**
56
67
  * Singleton object with small methods for creating, mapping or handling
57
68
  * `ResultTupleObj` instances.
@@ -85,7 +96,7 @@ export const Result = {
85
96
  if (error) {
86
97
  return Fail(error);
87
98
  }
88
- return Success(mapFn(resultValue));
99
+ return catch_(() => mapFn(resultValue));
89
100
  },
90
101
  /**
91
102
  * Unwraps a discriminated [error, result] `Result.Tuple`-like object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Misc. JS/TS helpers used by Reykjavík City's web dev teams.",
5
5
  "main": "index.js",
6
6
  "repository": "ssh://git@github.com:reykjavikcity/webtools.git",