lissa 1.0.6 → 1.1.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/README.md CHANGED
@@ -135,6 +135,8 @@ Checkout https://developer.mozilla.org/en-US/docs/Web/API/RequestInit for availa
135
135
  | paramsSerializer | "simple", "extended" or Function | "simple" | How to serialize the query params
136
136
  | urlBuilder | "simple", "extended" or Function | "simple" | How to build the final fetch url from the defined "baseURL" and "url"
137
137
  | responseType | "json", "text", "file" or "raw" | "json" | The type of data that the server will respond with
138
+ | replacer | Function or Array | undefined | A function or array to transform values during JSON.stringify
139
+ | reviver | Function | undefined | A function to transform values during JSON.parse
138
140
  | timeout | number | undefined | Specify the number of milliseconds before the request gets aborted
139
141
  | signal | AbortSignal | undefined | Cancel/Abort running requests
140
142
  | body | object, buffer, stream, file, etc. | undefined | Request body
@@ -150,6 +152,41 @@ Set the urlBuilder option to "simple", "extended" or a custom build function.
150
152
 
151
153
  Make sure to not forget a needed slash using "simple". If using "extended" be careful with leading and trailing slashes in urls, the baseURL and also with sub paths in the baseURL. For example `new URL("todos", "http://api.example.com/v2")` and `new URL("/todos", "http://api.example.com/v2/")` both results in a fetch to `"http://api.example.com/todos"`. Only `new URL("todos", "http://api.example.com/v2/")` will result in a fetch to the expected `"http://api.example.com/v2/todos"`.
152
154
 
155
+ #### replacer option
156
+ The replacer option allows you to control how values are stringified when sending JSON data. It works exactly like the second parameter of JSON.stringify(). You can provide either a function to transform values or an array to filter properties.
157
+
158
+ ```js
159
+ // Using a function to filter out private properties
160
+ const lissa = Lissa.create({
161
+ baseURL: "https://api.example.com",
162
+ replacer: (key, value) => {
163
+ if (key.startsWith('_')) return undefined; // Exclude private properties
164
+ return value;
165
+ }
166
+ });
167
+ ```
168
+
169
+ #### reviver option
170
+ The reviver option allows you to transform values when parsing JSON responses. It works exactly like the second parameter of JSON.parse(). This is particularly useful for converting ISO date strings back into Date objects or handling other custom data types.
171
+
172
+ ```js
173
+ // Using a reviver to convert ISO date strings to Date objects
174
+ const lissa = Lissa.create({
175
+ baseURL: "https://api.example.com",
176
+ reviver: (key, value) => {
177
+ // Check if value is an ISO 8601 date string
178
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
179
+ return new Date(value);
180
+ }
181
+ return value;
182
+ }
183
+ });
184
+
185
+ const { data } = await lissa.get("/events");
186
+ // data.createdAt is now a Date object instead of a string
187
+ console.log(data.createdAt instanceof Date); // true
188
+ ```
189
+
153
190
  ### `result` Object/Error
154
191
 
155
192
  Every request returns a promise which gets fulfilled into a result object or rejected into a result error.
@@ -682,6 +719,69 @@ publicClient.use(Lissa.dedupe());
682
719
 
683
720
  See the `examples/` directory for more usage examples, including browser-specific code and advanced plugin usage.
684
721
 
722
+ ### Using Reviver to Convert Date Strings
723
+
724
+ A common use case for the reviver function is to automatically convert ISO date strings back into JavaScript Date objects when receiving API responses.
725
+
726
+ ```js
727
+ import Lissa from "lissa";
728
+
729
+ // Create a reviver function that detects ISO 8601 date strings
730
+ function dateReviver(key, value) {
731
+ // Pattern to match ISO 8601 date strings like "2026-07-01T12:34:56.789Z"
732
+ const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
733
+
734
+ if (typeof value === 'string' && isoDatePattern.test(value)) {
735
+ return new Date(value);
736
+ }
737
+
738
+ return value;
739
+ }
740
+
741
+ // Create a Lissa instance with the reviver
742
+ const lissa = Lissa.create({
743
+ baseURL: "https://api.example.com",
744
+ reviver: dateReviver
745
+ });
746
+
747
+ // Example API response that contains date strings:
748
+ // {
749
+ // "id": 123,
750
+ // "title": "Meeting",
751
+ // "createdAt": "2026-07-01T10:00:00.000Z",
752
+ // "updatedAt": "2026-07-01T15:30:00.000Z",
753
+ // "participants": [
754
+ // { "name": "Alice", "joinedAt": "2026-07-01T10:05:00.000Z" },
755
+ // { "name": "Bob", "joinedAt": "2026-07-01T10:10:00.000Z" }
756
+ // ]
757
+ // }
758
+
759
+ const { data: meeting } = await lissa.get("/meetings/123");
760
+
761
+ // All date strings are automatically converted to Date objects
762
+ console.log(meeting.createdAt instanceof Date); // true
763
+ console.log(meeting.updatedAt instanceof Date); // true
764
+ console.log(meeting.createdAt.toLocaleDateString()); // "7/1/2026"
765
+
766
+ // Works with nested objects too
767
+ console.log(meeting.participants[0].joinedAt instanceof Date); // true
768
+
769
+ // You can now use Date methods directly
770
+ const duration = meeting.updatedAt - meeting.createdAt;
771
+ console.log(`Meeting lasted ${duration / 1000 / 60} minutes`);
772
+
773
+ // The reviver can also be set per-request
774
+ const { data: event } = await lissa.get("/events/456", {
775
+ reviver: (key, value) => {
776
+ // Custom reviver for this specific request
777
+ if (key === "startDate" || key === "endDate") {
778
+ return new Date(value);
779
+ }
780
+ return value;
781
+ }
782
+ });
783
+ ```
784
+
685
785
  ## Browser Support
686
786
 
687
787
  Lissa works in all modern browsers that support the following APIs:
@@ -13,6 +13,8 @@ export default {
13
13
  'paramsSerializer': 'simple',
14
14
  'urlBuilder': 'simple',
15
15
  'responseType': 'json',
16
+ 'replacer': undefined,
17
+ 'reviver': undefined,
16
18
 
17
19
  'get': basic(),
18
20
  'post': basic(),
@@ -72,6 +72,14 @@ export default class LissaRequest extends OpenPromise {
72
72
  return this.#addOptions({ body });
73
73
  }
74
74
 
75
+ reviver(reviver) {
76
+ return this.#addOptions({ reviver });
77
+ }
78
+
79
+ replacer(replacer) {
80
+ return this.#addOptions({ replacer });
81
+ }
82
+
75
83
  timeout(timeout) {
76
84
  return this.#addOptions({ timeout });
77
85
  }
@@ -95,7 +103,7 @@ export default class LissaRequest extends OpenPromise {
95
103
  #addOptions(options) {
96
104
  if (this.#locked) {
97
105
  console.warn(new Error('Request options cannot be changed anymore after execution start!'));
98
- return;
106
+ return this;
99
107
  }
100
108
 
101
109
  if (options.headers) options.headers = new Headers(options.headers);
@@ -162,13 +170,15 @@ export default class LissaRequest extends OpenPromise {
162
170
  options.headers.set('Content-Type', 'application/x-www-form-urlencoded');
163
171
  }
164
172
  else if (options.body.constructor === Object) {
165
- options.body = JSON.stringify(options.body);
173
+ options.body = JSON.stringify(options.body, options.replacer);
166
174
  }
167
175
  }
168
176
  else {
169
177
  options.headers.delete('Content-Type');
170
178
  }
171
179
 
180
+ delete options.replacer;
181
+
172
182
  if (options.timeout) {
173
183
  if (options.signal) {
174
184
  options.signal = AbortSignal.any([
@@ -183,8 +193,8 @@ export default class LissaRequest extends OpenPromise {
183
193
  delete options.timeout;
184
194
  }
185
195
 
186
- let baseURL, url, params, urlBuilder, adapter;
187
- ({ baseURL, url, params, urlBuilder, adapter, ...options } = options);
196
+ let baseURL, url, params, urlBuilder, adapter, reviver;
197
+ ({ baseURL, url, params, urlBuilder, adapter, reviver, ...options } = options);
188
198
 
189
199
  const fetchUrl = ((
190
200
  baseURL,
@@ -204,8 +214,8 @@ export default class LissaRequest extends OpenPromise {
204
214
 
205
215
  // Actual fetch
206
216
  let returnValue = adapter === 'xhr'
207
- ? await this.#executeXhr({ url: new URL(fetchUrl), ...options })
208
- : await this.#executeFetch({ url: new URL(fetchUrl), ...options });
217
+ ? await this.#executeXhr({ url: new URL(fetchUrl), reviver, ...options })
218
+ : await this.#executeFetch({ url: new URL(fetchUrl), reviver, ...options });
209
219
 
210
220
  returnValue.options = $options;
211
221
 
@@ -228,7 +238,7 @@ export default class LissaRequest extends OpenPromise {
228
238
  return returnValue;
229
239
  }
230
240
 
231
- async #executeFetch({ url, responseType, onUploadProgress, onDownloadProgress, ...options }) {
241
+ async #executeFetch({ url, responseType, onUploadProgress, onDownloadProgress, reviver, ...options }) {
232
242
  let request = { url, options };
233
243
 
234
244
  for (const hook of this.#lissa.beforeFetchHooks) {
@@ -297,7 +307,7 @@ export default class LissaRequest extends OpenPromise {
297
307
  if (!data) break;
298
308
 
299
309
  try {
300
- data = JSON.parse(data);
310
+ data = JSON.parse(data, reviver);
301
311
  }
302
312
  catch (error) {
303
313
  data = null;
@@ -326,7 +336,7 @@ export default class LissaRequest extends OpenPromise {
326
336
  return returnValue;
327
337
  }
328
338
 
329
- async #executeXhr({ url, ...options }) {
339
+ async #executeXhr({ url, reviver, ...options }) {
330
340
  let request = { url, options };
331
341
 
332
342
  for (const hook of this.#lissa.beforeFetchHooks) {
@@ -377,7 +387,11 @@ export default class LissaRequest extends OpenPromise {
377
387
 
378
388
  if (options.signal) options.signal.addEventListener('abort', () => xhr.abort());
379
389
 
380
- xhr.responseType = options.responseType === 'raw' || options.responseType === 'file' ? 'blob' : options.responseType;
390
+ xhr.responseType = options.responseType === 'json'
391
+ ? 'text'
392
+ : options.responseType === 'raw' || options.responseType === 'file'
393
+ ? 'blob'
394
+ : options.responseType;
381
395
 
382
396
  if (options.onUploadProgress) {
383
397
  xhr.upload.addEventListener('progress', (evt) => {
@@ -404,6 +418,16 @@ export default class LissaRequest extends OpenPromise {
404
418
  returnValue.status = response.status;
405
419
  returnValue.data = response.body;
406
420
 
421
+ if (options.responseType === 'json' && response.body) {
422
+ try {
423
+ returnValue.data = JSON.parse(response.body, reviver);
424
+ }
425
+ catch (error) {
426
+ returnValue.data = null;
427
+ console.error(error);
428
+ }
429
+ }
430
+
407
431
  if (options.responseType === 'file') {
408
432
  const type = response.headers.get('Content-Type');
409
433
  const filename = getFilenameFromContentDisposition(response.headers.get('Content-Disposition'));
package/lib/index.d.ts CHANGED
@@ -53,6 +53,8 @@ export type LissaOptionsInit = Omit<RequestInit, 'method' | 'body'> & {
53
53
  paramsSerializer?: 'simple' | 'extended' | ((params: Params) => string);
54
54
  urlBuilder?: 'simple' | 'extended' | ((url: string, baseURL: string) => string | URL);
55
55
  responseType?: 'json' | 'text' | 'file' | 'raw';
56
+ replacer?: (this: any, key: string, value: any) => any | (string | number)[];
57
+ reviver?: (this: any, key: string, value: any) => any;
56
58
  timeout?: number;
57
59
  onUploadProgress?: (uploaded: number, total: number) => void;
58
60
  onDownloadProgress?: (downloaded: number, total: number) => void;
@@ -193,6 +195,20 @@ export declare class LissaRequest<RT = ResultValue<ResultData>> extends Promise<
193
195
  */
194
196
  body(body: BodyInit | JsonStringifyableObject): LissaRequest<RT>;
195
197
 
198
+ /**
199
+ * Set a replacer function or array to transform values during JSON.stringify
200
+ *
201
+ * Works like the second parameter of JSON.stringify()
202
+ */
203
+ replacer(replacer: (this: any, key: string, value: any) => any | (string | number)[]): LissaRequest<RT>;
204
+
205
+ /**
206
+ * Set a reviver function to transform values during JSON.parse
207
+ *
208
+ * Works like the second parameter of JSON.parse()
209
+ */
210
+ reviver(reviver: (this: any, key: string, value: any) => any): LissaRequest<RT>;
211
+
196
212
  /**
197
213
  * Set request timeout in milliseconds
198
214
  *
@@ -22,7 +22,12 @@ export function deepMerge(...objects) {
22
22
  let merged;
23
23
 
24
24
  for (const obj of objects) {
25
- if (obj == null) continue;
25
+ if (obj === undefined) continue;
26
+
27
+ if (obj === null) {
28
+ merged = obj;
29
+ continue;
30
+ }
26
31
 
27
32
  if (obj instanceof AbortSignal) {
28
33
  if (!merged) merged = obj;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lissa",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "A lightweight, extensible HTTP client for modern JavaScript applications with fluent api syntax, hooks, plugins and more.",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",