@naturalcycles/js-lib 14.252.1 → 14.254.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/dist/env.js +2 -0
- package/dist/error/error.util.js +3 -4
- package/dist/http/fetcher.d.ts +2 -1
- package/dist/http/fetcher.js +18 -4
- package/dist/http/fetcher.model.d.ts +31 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/number/number.util.js +1 -0
- package/dist/object/object.util.js +4 -1
- package/dist/string/slugify.d.ts +19 -0
- package/dist/string/slugify.js +69 -0
- package/dist-esm/env.js +2 -0
- package/dist-esm/error/error.util.js +4 -4
- package/dist-esm/http/fetcher.js +20 -5
- package/dist-esm/index.js +1 -0
- package/dist-esm/number/number.util.js +1 -0
- package/dist-esm/object/object.util.js +4 -1
- package/dist-esm/string/slugify.js +66 -0
- package/package.json +1 -1
- package/src/env.ts +2 -0
- package/src/error/error.util.ts +3 -5
- package/src/http/fetcher.model.ts +35 -0
- package/src/http/fetcher.ts +23 -4
- package/src/index.ts +1 -0
- package/src/number/number.util.ts +1 -0
- package/src/object/object.util.ts +2 -0
- package/src/string/slugify.ts +107 -0
package/dist/env.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.isClientSide = isClientSide;
|
|
|
9
9
|
* Will return `false` in the Browser.
|
|
10
10
|
*/
|
|
11
11
|
function isServerSide() {
|
|
12
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
12
13
|
return typeof window === 'undefined';
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
@@ -18,5 +19,6 @@ function isServerSide() {
|
|
|
18
19
|
* Will return `false` in Node.js.
|
|
19
20
|
*/
|
|
20
21
|
function isClientSide() {
|
|
22
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
21
23
|
return typeof window !== 'undefined';
|
|
22
24
|
}
|
package/dist/error/error.util.js
CHANGED
|
@@ -219,10 +219,9 @@ function _isErrorLike(o) {
|
|
|
219
219
|
function _errorDataAppend(err, data) {
|
|
220
220
|
if (!data)
|
|
221
221
|
return err;
|
|
222
|
-
err.data
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
222
|
+
err.data ||= {}; // create err.data if it doesn't exist
|
|
223
|
+
// Using Object.assign instead of ...data to not override err.data's non-enumerable properties
|
|
224
|
+
Object.assign(err.data, data);
|
|
226
225
|
return err;
|
|
227
226
|
}
|
|
228
227
|
/**
|
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/// <reference lib="dom.iterable" preserve="true" />
|
|
4
4
|
import { HttpRequestError } from '../error/error.util';
|
|
5
5
|
import { ErrorDataTuple } from '../types';
|
|
6
|
-
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse, RequestInitNormalized } from './fetcher.model';
|
|
6
|
+
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOnErrorHook, FetcherOptions, FetcherResponse, RequestInitNormalized } from './fetcher.model';
|
|
7
7
|
/**
|
|
8
8
|
* Experimental wrapper around Fetch.
|
|
9
9
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
@@ -24,6 +24,7 @@ export declare class Fetcher {
|
|
|
24
24
|
onBeforeRequest(hook: FetcherBeforeRequestHook): this;
|
|
25
25
|
onAfterResponse(hook: FetcherAfterResponseHook): this;
|
|
26
26
|
onBeforeRetry(hook: FetcherBeforeRetryHook): this;
|
|
27
|
+
onError(hook: FetcherOnErrorHook): this;
|
|
27
28
|
cfg: FetcherNormalizedCfg;
|
|
28
29
|
static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
|
|
29
30
|
get: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
package/dist/http/fetcher.js
CHANGED
|
@@ -98,6 +98,11 @@ class Fetcher {
|
|
|
98
98
|
(this.cfg.hooks.beforeRetry ||= []).push(hook);
|
|
99
99
|
return this;
|
|
100
100
|
}
|
|
101
|
+
onError(hook) {
|
|
102
|
+
;
|
|
103
|
+
(this.cfg.hooks.onError ||= []).push(hook);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
101
106
|
static create(cfg = {}) {
|
|
102
107
|
return new Fetcher(cfg);
|
|
103
108
|
}
|
|
@@ -195,12 +200,12 @@ class Fetcher {
|
|
|
195
200
|
abortController.abort(new error_util_1.TimeoutError(`request timed out after ${timeoutSeconds} sec`));
|
|
196
201
|
}, timeoutSeconds * 1000);
|
|
197
202
|
}
|
|
198
|
-
if (
|
|
203
|
+
if (req.logRequest) {
|
|
199
204
|
const { retryAttempt } = res.retryStatus;
|
|
200
205
|
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
201
206
|
.filter(Boolean)
|
|
202
207
|
.join(' '));
|
|
203
|
-
if (
|
|
208
|
+
if (req.logRequestBody && req.init.body) {
|
|
204
209
|
logger.log(req.init.body); // todo: check if we can _inspect it
|
|
205
210
|
}
|
|
206
211
|
}
|
|
@@ -248,6 +253,13 @@ class Fetcher {
|
|
|
248
253
|
await this.onNotOkResponse(res);
|
|
249
254
|
}
|
|
250
255
|
}
|
|
256
|
+
if (res.err) {
|
|
257
|
+
(0, error_util_1._errorDataAppend)(res.err, req.errorData);
|
|
258
|
+
req.onError?.(res.err);
|
|
259
|
+
for (const hook of this.cfg.hooks.onError || []) {
|
|
260
|
+
await hook(res.err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
251
263
|
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
252
264
|
await hook(res);
|
|
253
265
|
}
|
|
@@ -294,7 +306,7 @@ class Fetcher {
|
|
|
294
306
|
}
|
|
295
307
|
res.retryStatus.retryStopped = true;
|
|
296
308
|
// res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
|
|
297
|
-
if ((!res.err || !req.throwHttpErrors) &&
|
|
309
|
+
if ((!res.err || !req.throwHttpErrors) && req.logResponse) {
|
|
298
310
|
const { retryAttempt } = res.retryStatus;
|
|
299
311
|
const { logger } = this.cfg;
|
|
300
312
|
logger.log([
|
|
@@ -306,7 +318,7 @@ class Fetcher {
|
|
|
306
318
|
]
|
|
307
319
|
.filter(Boolean)
|
|
308
320
|
.join(' '));
|
|
309
|
-
if (
|
|
321
|
+
if (req.logResponseBody && res.body !== undefined) {
|
|
310
322
|
logger.log(res.body);
|
|
311
323
|
}
|
|
312
324
|
}
|
|
@@ -542,6 +554,7 @@ class Fetcher {
|
|
|
542
554
|
},
|
|
543
555
|
hooks: {},
|
|
544
556
|
throwHttpErrors: true,
|
|
557
|
+
errorData: {},
|
|
545
558
|
}, (0, object_util_1._omit)(cfg, ['method', 'credentials', 'headers', 'redirect', 'logger']));
|
|
546
559
|
norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
|
|
547
560
|
return norm;
|
|
@@ -561,6 +574,7 @@ class Fetcher {
|
|
|
561
574
|
'logResponseBody',
|
|
562
575
|
'debug',
|
|
563
576
|
'throwHttpErrors',
|
|
577
|
+
'errorData',
|
|
564
578
|
]),
|
|
565
579
|
started: Date.now(),
|
|
566
580
|
...(0, object_util_1._omit)(opt, ['method', 'headers', 'credentials']),
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/// <reference lib="es2022" preserve="true" />
|
|
2
2
|
/// <reference lib="dom" preserve="true" />
|
|
3
|
+
import type { ErrorData } from '../error/error.model';
|
|
3
4
|
import type { CommonLogger } from '../log/commonLogger';
|
|
4
5
|
import type { Promisable } from '../typeFest';
|
|
5
6
|
import type { AnyObject, NumberOfMilliseconds, Reviver, UnixTimestampMillisNumber } from '../types';
|
|
6
7
|
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
7
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'debug' | 'redirect' | 'credentials' | 'throwHttpErrors'> {
|
|
8
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'debug' | 'redirect' | 'credentials' | 'throwHttpErrors' | 'errorData'> {
|
|
8
9
|
logger: CommonLogger;
|
|
9
10
|
searchParams: Record<string, any>;
|
|
10
11
|
}
|
|
11
12
|
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
|
|
12
13
|
export type FetcherAfterResponseHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
|
|
13
14
|
export type FetcherBeforeRetryHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Allows to mutate the error.
|
|
17
|
+
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
|
|
18
|
+
*/
|
|
19
|
+
export type FetcherOnErrorHook = (err: Error) => Promisable<void>;
|
|
14
20
|
export interface FetcherCfg {
|
|
15
21
|
/**
|
|
16
22
|
* Should **not** contain trailing slash.
|
|
@@ -35,7 +41,17 @@ export interface FetcherCfg {
|
|
|
35
41
|
* Allows to mutate res.retryStatus to override retry behavior.
|
|
36
42
|
*/
|
|
37
43
|
beforeRetry?: FetcherBeforeRetryHook[];
|
|
44
|
+
onError?: FetcherOnErrorHook[];
|
|
38
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* If Fetcher has an error - `errorData` object will be appended to the error data.
|
|
48
|
+
* Like this:
|
|
49
|
+
*
|
|
50
|
+
* _errorDataAppend(err, cfg.errorData)
|
|
51
|
+
*
|
|
52
|
+
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
|
|
53
|
+
*/
|
|
54
|
+
errorData?: ErrorData | undefined;
|
|
39
55
|
/**
|
|
40
56
|
* If true - enables all possible logging.
|
|
41
57
|
*/
|
|
@@ -188,6 +204,20 @@ export interface FetcherOptions {
|
|
|
188
204
|
* Set to false to not throw on `!Response.ok`, but simply return `Response.body` as-is (json parsed, etc).
|
|
189
205
|
*/
|
|
190
206
|
throwHttpErrors?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* If Fetcher has an error - `errorData` object will be appended to the error data.
|
|
209
|
+
* Like this:
|
|
210
|
+
*
|
|
211
|
+
* _errorDataAppend(err, cfg.errorData)
|
|
212
|
+
*
|
|
213
|
+
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
|
|
214
|
+
*/
|
|
215
|
+
errorData?: ErrorData;
|
|
216
|
+
/**
|
|
217
|
+
* Allows to mutate the error.
|
|
218
|
+
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
|
|
219
|
+
*/
|
|
220
|
+
onError?: FetcherOnErrorHook;
|
|
191
221
|
}
|
|
192
222
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
193
223
|
method: HttpMethod;
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export * from './string/pupa';
|
|
|
74
74
|
export * from './string/readingTime';
|
|
75
75
|
export * from './string/regex';
|
|
76
76
|
export * from './string/safeJsonStringify';
|
|
77
|
+
export * from './string/slugify';
|
|
77
78
|
export * from './string/string.util';
|
|
78
79
|
export * from './string/stringify';
|
|
79
80
|
export * from './string/url.util';
|
package/dist/index.js
CHANGED
|
@@ -78,6 +78,7 @@ tslib_1.__exportStar(require("./string/pupa"), exports);
|
|
|
78
78
|
tslib_1.__exportStar(require("./string/readingTime"), exports);
|
|
79
79
|
tslib_1.__exportStar(require("./string/regex"), exports);
|
|
80
80
|
tslib_1.__exportStar(require("./string/safeJsonStringify"), exports);
|
|
81
|
+
tslib_1.__exportStar(require("./string/slugify"), exports);
|
|
81
82
|
tslib_1.__exportStar(require("./string/string.util"), exports);
|
|
82
83
|
tslib_1.__exportStar(require("./string/stringify"), exports);
|
|
83
84
|
tslib_1.__exportStar(require("./string/url.util"), exports);
|
|
@@ -366,6 +366,7 @@ function _get(obj = {}, path = '') {
|
|
|
366
366
|
* Based on: https://stackoverflow.com/a/54733755/4919972
|
|
367
367
|
*/
|
|
368
368
|
function _set(obj, path, value) {
|
|
369
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
369
370
|
if (!obj || Object(obj) !== obj || !path)
|
|
370
371
|
return obj; // When obj is not an object
|
|
371
372
|
// If not yet an array, get the keys from the string-path
|
|
@@ -377,7 +378,9 @@ function _set(obj, path, value) {
|
|
|
377
378
|
}
|
|
378
379
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
379
380
|
;
|
|
380
|
-
path.slice(0, -1).reduce((a, c, i) =>
|
|
381
|
+
path.slice(0, -1).reduce((a, c, i) =>
|
|
382
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
383
|
+
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
381
384
|
? // Yes: then follow that path
|
|
382
385
|
a[c]
|
|
383
386
|
: // No: create the key. Is the next key a potential array-index?
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SlugifyOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Default: `-`
|
|
4
|
+
*/
|
|
5
|
+
separator?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Default: true
|
|
8
|
+
*/
|
|
9
|
+
lowercase?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Default: true
|
|
12
|
+
*/
|
|
13
|
+
decamelize?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Default: []
|
|
16
|
+
*/
|
|
17
|
+
preserveCharacters?: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function _slugify(s: string, opt?: SlugifyOptions): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Credit to (adopted from): https://github.com/sindresorhus/slugify/
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports._slugify = _slugify;
|
|
5
|
+
function _slugify(s, opt = {}) {
|
|
6
|
+
opt = {
|
|
7
|
+
separator: '-',
|
|
8
|
+
lowercase: true,
|
|
9
|
+
decamelize: true,
|
|
10
|
+
preserveCharacters: [],
|
|
11
|
+
...opt,
|
|
12
|
+
};
|
|
13
|
+
if (opt.decamelize) {
|
|
14
|
+
s = decamelize(s);
|
|
15
|
+
}
|
|
16
|
+
const patternSlug = buildPatternSlug(opt);
|
|
17
|
+
if (opt.lowercase) {
|
|
18
|
+
s = s.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
// based on https://stackoverflow.com/a/23633850/4919972
|
|
21
|
+
// Combining Diacritical Marks
|
|
22
|
+
// https://www.unicode.org/charts/PDF/U0300.pdf
|
|
23
|
+
// biome-ignore lint/suspicious/noMisleadingCharacterClass: ok
|
|
24
|
+
s = s.normalize('NFKD').replaceAll(/[\u0300-\u036F]/g, '');
|
|
25
|
+
// Detect contractions/possessives by looking for any word followed by a `'t`
|
|
26
|
+
// or `'s` in isolation and then remove it.
|
|
27
|
+
s = s.replaceAll(/([a-zA-Z\d]+)'([ts])(\s|$)/g, '$1$2$3');
|
|
28
|
+
s = s.replace(patternSlug, opt.separator);
|
|
29
|
+
s = s.replaceAll('\\', '');
|
|
30
|
+
if (opt.separator) {
|
|
31
|
+
s = removeMootSeparators(s, opt.separator);
|
|
32
|
+
}
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
function buildPatternSlug(options) {
|
|
36
|
+
let negationSetPattern = String.raw `a-z\d`;
|
|
37
|
+
negationSetPattern += options.lowercase ? '' : 'A-Z';
|
|
38
|
+
if (options.preserveCharacters.length > 0) {
|
|
39
|
+
for (const character of options.preserveCharacters) {
|
|
40
|
+
if (character === options.separator) {
|
|
41
|
+
throw new Error(`The separator character \`${options.separator}\` cannot be included in preserved characters: ${options.preserveCharacters}`);
|
|
42
|
+
}
|
|
43
|
+
negationSetPattern += escapeStringRegexp(character);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return new RegExp(`[^${negationSetPattern}]+`, 'g');
|
|
47
|
+
}
|
|
48
|
+
function removeMootSeparators(s, separator) {
|
|
49
|
+
const escapedSeparator = escapeStringRegexp(separator);
|
|
50
|
+
return s
|
|
51
|
+
.replaceAll(new RegExp(`${escapedSeparator}{2,}`, 'g'), separator)
|
|
52
|
+
.replaceAll(new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'), '');
|
|
53
|
+
}
|
|
54
|
+
function decamelize(s) {
|
|
55
|
+
return (s
|
|
56
|
+
// Separate capitalized words.
|
|
57
|
+
.replaceAll(/([A-Z]{2,})(\d+)/g, '$1 $2')
|
|
58
|
+
.replaceAll(/([a-z\d]+)([A-Z]{2,})/g, '$1 $2')
|
|
59
|
+
.replaceAll(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
60
|
+
// `[a-rt-z]` matches all lowercase characters except `s`.
|
|
61
|
+
// This avoids matching plural acronyms like `APIs`.
|
|
62
|
+
.replaceAll(/([A-Z]+)([A-Z][a-rt-z\d]+)/g, '$1 $2'));
|
|
63
|
+
}
|
|
64
|
+
// based on: https://github.com/sindresorhus/escape-string-regexp/
|
|
65
|
+
function escapeStringRegexp(s) {
|
|
66
|
+
// Escape characters with special meaning either inside or outside character sets.
|
|
67
|
+
// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
|
|
68
|
+
return s.replaceAll(/[|\\{}()[\]^$+*?.]/g, String.raw `\$&`).replaceAll('-', String.raw `\x2d`);
|
|
69
|
+
}
|
package/dist-esm/env.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Will return `false` in the Browser.
|
|
6
6
|
*/
|
|
7
7
|
export function isServerSide() {
|
|
8
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
8
9
|
return typeof window === 'undefined';
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
@@ -14,5 +15,6 @@ export function isServerSide() {
|
|
|
14
15
|
* Will return `false` in Node.js.
|
|
15
16
|
*/
|
|
16
17
|
export function isClientSide() {
|
|
18
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
17
19
|
return typeof window !== 'undefined';
|
|
18
20
|
}
|
|
@@ -204,12 +204,12 @@ export function _isErrorLike(o) {
|
|
|
204
204
|
* }
|
|
205
205
|
*/
|
|
206
206
|
export function _errorDataAppend(err, data) {
|
|
207
|
+
var _a;
|
|
207
208
|
if (!data)
|
|
208
209
|
return err;
|
|
209
|
-
err.data = {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
210
|
+
(_a = err).data || (_a.data = {}); // create err.data if it doesn't exist
|
|
211
|
+
// Using Object.assign instead of ...data to not override err.data's non-enumerable properties
|
|
212
|
+
Object.assign(err.data, data);
|
|
213
213
|
return err;
|
|
214
214
|
}
|
|
215
215
|
/**
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
var _a;
|
|
5
5
|
import { isServerSide } from '../env';
|
|
6
6
|
import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert';
|
|
7
|
-
import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject, HttpRequestError, TimeoutError, UnexpectedPassError, } from '../error/error.util';
|
|
7
|
+
import { _anyToError, _anyToErrorObject, _errorDataAppend, _errorLikeToErrorObject, HttpRequestError, TimeoutError, UnexpectedPassError, } from '../error/error.util';
|
|
8
8
|
import { _clamp } from '../number/number.util';
|
|
9
9
|
import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, _pick, } from '../object/object.util';
|
|
10
10
|
import { pDelay } from '../promise/pDelay';
|
|
@@ -90,6 +90,12 @@ export class Fetcher {
|
|
|
90
90
|
((_b = this.cfg.hooks).beforeRetry || (_b.beforeRetry = [])).push(hook);
|
|
91
91
|
return this;
|
|
92
92
|
}
|
|
93
|
+
onError(hook) {
|
|
94
|
+
var _b;
|
|
95
|
+
;
|
|
96
|
+
((_b = this.cfg.hooks).onError || (_b.onError = [])).push(hook);
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
93
99
|
static create(cfg = {}) {
|
|
94
100
|
return new _a(cfg);
|
|
95
101
|
}
|
|
@@ -187,12 +193,12 @@ export class Fetcher {
|
|
|
187
193
|
abortController.abort(new TimeoutError(`request timed out after ${timeoutSeconds} sec`));
|
|
188
194
|
}, timeoutSeconds * 1000);
|
|
189
195
|
}
|
|
190
|
-
if (
|
|
196
|
+
if (req.logRequest) {
|
|
191
197
|
const { retryAttempt } = res.retryStatus;
|
|
192
198
|
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
193
199
|
.filter(Boolean)
|
|
194
200
|
.join(' '));
|
|
195
|
-
if (
|
|
201
|
+
if (req.logRequestBody && req.init.body) {
|
|
196
202
|
logger.log(req.init.body); // todo: check if we can _inspect it
|
|
197
203
|
}
|
|
198
204
|
}
|
|
@@ -240,6 +246,13 @@ export class Fetcher {
|
|
|
240
246
|
await this.onNotOkResponse(res);
|
|
241
247
|
}
|
|
242
248
|
}
|
|
249
|
+
if (res.err) {
|
|
250
|
+
_errorDataAppend(res.err, req.errorData);
|
|
251
|
+
req.onError?.(res.err);
|
|
252
|
+
for (const hook of this.cfg.hooks.onError || []) {
|
|
253
|
+
await hook(res.err);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
243
256
|
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
244
257
|
await hook(res);
|
|
245
258
|
}
|
|
@@ -286,7 +299,7 @@ export class Fetcher {
|
|
|
286
299
|
}
|
|
287
300
|
res.retryStatus.retryStopped = true;
|
|
288
301
|
// res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
|
|
289
|
-
if ((!res.err || !req.throwHttpErrors) &&
|
|
302
|
+
if ((!res.err || !req.throwHttpErrors) && req.logResponse) {
|
|
290
303
|
const { retryAttempt } = res.retryStatus;
|
|
291
304
|
const { logger } = this.cfg;
|
|
292
305
|
logger.log([
|
|
@@ -298,7 +311,7 @@ export class Fetcher {
|
|
|
298
311
|
]
|
|
299
312
|
.filter(Boolean)
|
|
300
313
|
.join(' '));
|
|
301
|
-
if (
|
|
314
|
+
if (req.logResponseBody && res.body !== undefined) {
|
|
302
315
|
logger.log(res.body);
|
|
303
316
|
}
|
|
304
317
|
}
|
|
@@ -534,6 +547,7 @@ export class Fetcher {
|
|
|
534
547
|
},
|
|
535
548
|
hooks: {},
|
|
536
549
|
throwHttpErrors: true,
|
|
550
|
+
errorData: {},
|
|
537
551
|
}, _omit(cfg, ['method', 'credentials', 'headers', 'redirect', 'logger']));
|
|
538
552
|
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
|
|
539
553
|
return norm;
|
|
@@ -554,6 +568,7 @@ export class Fetcher {
|
|
|
554
568
|
'logResponseBody',
|
|
555
569
|
'debug',
|
|
556
570
|
'throwHttpErrors',
|
|
571
|
+
'errorData',
|
|
557
572
|
]),
|
|
558
573
|
started: Date.now(),
|
|
559
574
|
..._omit(opt, ['method', 'headers', 'credentials']),
|
package/dist-esm/index.js
CHANGED
|
@@ -74,6 +74,7 @@ export * from './string/pupa';
|
|
|
74
74
|
export * from './string/readingTime';
|
|
75
75
|
export * from './string/regex';
|
|
76
76
|
export * from './string/safeJsonStringify';
|
|
77
|
+
export * from './string/slugify';
|
|
77
78
|
export * from './string/string.util';
|
|
78
79
|
export * from './string/stringify';
|
|
79
80
|
export * from './string/url.util';
|
|
@@ -45,6 +45,7 @@ export function _isBetween(x, min, max, incl = '[)') {
|
|
|
45
45
|
return x > min && x < max;
|
|
46
46
|
}
|
|
47
47
|
export function _clamp(x, minIncl, maxIncl) {
|
|
48
|
+
// eslint-disable-next-line unicorn/prefer-math-min-max
|
|
48
49
|
return x <= minIncl ? minIncl : x >= maxIncl ? maxIncl : x;
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
@@ -337,6 +337,7 @@ export function _get(obj = {}, path = '') {
|
|
|
337
337
|
* Based on: https://stackoverflow.com/a/54733755/4919972
|
|
338
338
|
*/
|
|
339
339
|
export function _set(obj, path, value) {
|
|
340
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
340
341
|
if (!obj || Object(obj) !== obj || !path)
|
|
341
342
|
return obj; // When obj is not an object
|
|
342
343
|
// If not yet an array, get the keys from the string-path
|
|
@@ -348,7 +349,9 @@ export function _set(obj, path, value) {
|
|
|
348
349
|
}
|
|
349
350
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
350
351
|
;
|
|
351
|
-
path.slice(0, -1).reduce((a, c, i) =>
|
|
352
|
+
path.slice(0, -1).reduce((a, c, i) =>
|
|
353
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
354
|
+
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
352
355
|
? // Yes: then follow that path
|
|
353
356
|
a[c]
|
|
354
357
|
: // No: create the key. Is the next key a potential array-index?
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Credit to (adopted from): https://github.com/sindresorhus/slugify/
|
|
2
|
+
export function _slugify(s, opt = {}) {
|
|
3
|
+
opt = {
|
|
4
|
+
separator: '-',
|
|
5
|
+
lowercase: true,
|
|
6
|
+
decamelize: true,
|
|
7
|
+
preserveCharacters: [],
|
|
8
|
+
...opt,
|
|
9
|
+
};
|
|
10
|
+
if (opt.decamelize) {
|
|
11
|
+
s = decamelize(s);
|
|
12
|
+
}
|
|
13
|
+
const patternSlug = buildPatternSlug(opt);
|
|
14
|
+
if (opt.lowercase) {
|
|
15
|
+
s = s.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
// based on https://stackoverflow.com/a/23633850/4919972
|
|
18
|
+
// Combining Diacritical Marks
|
|
19
|
+
// https://www.unicode.org/charts/PDF/U0300.pdf
|
|
20
|
+
// biome-ignore lint/suspicious/noMisleadingCharacterClass: ok
|
|
21
|
+
s = s.normalize('NFKD').replaceAll(/[\u0300-\u036F]/g, '');
|
|
22
|
+
// Detect contractions/possessives by looking for any word followed by a `'t`
|
|
23
|
+
// or `'s` in isolation and then remove it.
|
|
24
|
+
s = s.replaceAll(/([a-zA-Z\d]+)'([ts])(\s|$)/g, '$1$2$3');
|
|
25
|
+
s = s.replace(patternSlug, opt.separator);
|
|
26
|
+
s = s.replaceAll('\\', '');
|
|
27
|
+
if (opt.separator) {
|
|
28
|
+
s = removeMootSeparators(s, opt.separator);
|
|
29
|
+
}
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
function buildPatternSlug(options) {
|
|
33
|
+
let negationSetPattern = String.raw `a-z\d`;
|
|
34
|
+
negationSetPattern += options.lowercase ? '' : 'A-Z';
|
|
35
|
+
if (options.preserveCharacters.length > 0) {
|
|
36
|
+
for (const character of options.preserveCharacters) {
|
|
37
|
+
if (character === options.separator) {
|
|
38
|
+
throw new Error(`The separator character \`${options.separator}\` cannot be included in preserved characters: ${options.preserveCharacters}`);
|
|
39
|
+
}
|
|
40
|
+
negationSetPattern += escapeStringRegexp(character);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return new RegExp(`[^${negationSetPattern}]+`, 'g');
|
|
44
|
+
}
|
|
45
|
+
function removeMootSeparators(s, separator) {
|
|
46
|
+
const escapedSeparator = escapeStringRegexp(separator);
|
|
47
|
+
return s
|
|
48
|
+
.replaceAll(new RegExp(`${escapedSeparator}{2,}`, 'g'), separator)
|
|
49
|
+
.replaceAll(new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'), '');
|
|
50
|
+
}
|
|
51
|
+
function decamelize(s) {
|
|
52
|
+
return (s
|
|
53
|
+
// Separate capitalized words.
|
|
54
|
+
.replaceAll(/([A-Z]{2,})(\d+)/g, '$1 $2')
|
|
55
|
+
.replaceAll(/([a-z\d]+)([A-Z]{2,})/g, '$1 $2')
|
|
56
|
+
.replaceAll(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
57
|
+
// `[a-rt-z]` matches all lowercase characters except `s`.
|
|
58
|
+
// This avoids matching plural acronyms like `APIs`.
|
|
59
|
+
.replaceAll(/([A-Z]+)([A-Z][a-rt-z\d]+)/g, '$1 $2'));
|
|
60
|
+
}
|
|
61
|
+
// based on: https://github.com/sindresorhus/escape-string-regexp/
|
|
62
|
+
function escapeStringRegexp(s) {
|
|
63
|
+
// Escape characters with special meaning either inside or outside character sets.
|
|
64
|
+
// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
|
|
65
|
+
return s.replaceAll(/[|\\{}()[\]^$+*?.]/g, String.raw `\$&`).replaceAll('-', String.raw `\x2d`);
|
|
66
|
+
}
|
package/package.json
CHANGED
package/src/env.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Will return `false` in the Browser.
|
|
6
6
|
*/
|
|
7
7
|
export function isServerSide(): boolean {
|
|
8
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
8
9
|
return typeof window === 'undefined'
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -15,5 +16,6 @@ export function isServerSide(): boolean {
|
|
|
15
16
|
* Will return `false` in Node.js.
|
|
16
17
|
*/
|
|
17
18
|
export function isClientSide(): boolean {
|
|
19
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
18
20
|
return typeof window !== 'undefined'
|
|
19
21
|
}
|
package/src/error/error.util.ts
CHANGED
|
@@ -265,11 +265,9 @@ export function _isErrorLike(o: any): o is ErrorLike {
|
|
|
265
265
|
*/
|
|
266
266
|
export function _errorDataAppend<ERR>(err: ERR, data?: ErrorData): ERR {
|
|
267
267
|
if (!data) return err
|
|
268
|
-
;(err as any).data
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
268
|
+
;(err as any).data ||= {} // create err.data if it doesn't exist
|
|
269
|
+
// Using Object.assign instead of ...data to not override err.data's non-enumerable properties
|
|
270
|
+
Object.assign((err as any).data, data)
|
|
273
271
|
return err
|
|
274
272
|
}
|
|
275
273
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference lib="es2022" preserve="true" />
|
|
2
2
|
/// <reference lib="dom" preserve="true" />
|
|
3
3
|
|
|
4
|
+
import type { ErrorData } from '../error/error.model'
|
|
4
5
|
import type { CommonLogger } from '../log/commonLogger'
|
|
5
6
|
import type { Promisable } from '../typeFest'
|
|
6
7
|
import type { AnyObject, NumberOfMilliseconds, Reviver, UnixTimestampMillisNumber } from '../types'
|
|
@@ -20,6 +21,7 @@ export interface FetcherNormalizedCfg
|
|
|
20
21
|
| 'redirect'
|
|
21
22
|
| 'credentials'
|
|
22
23
|
| 'throwHttpErrors'
|
|
24
|
+
| 'errorData'
|
|
23
25
|
> {
|
|
24
26
|
logger: CommonLogger
|
|
25
27
|
searchParams: Record<string, any>
|
|
@@ -32,6 +34,11 @@ export type FetcherAfterResponseHook = <BODY = unknown>(
|
|
|
32
34
|
export type FetcherBeforeRetryHook = <BODY = unknown>(
|
|
33
35
|
res: FetcherResponse<BODY>,
|
|
34
36
|
) => Promisable<void>
|
|
37
|
+
/**
|
|
38
|
+
* Allows to mutate the error.
|
|
39
|
+
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
|
|
40
|
+
*/
|
|
41
|
+
export type FetcherOnErrorHook = (err: Error) => Promisable<void>
|
|
35
42
|
|
|
36
43
|
export interface FetcherCfg {
|
|
37
44
|
/**
|
|
@@ -58,8 +65,20 @@ export interface FetcherCfg {
|
|
|
58
65
|
* Allows to mutate res.retryStatus to override retry behavior.
|
|
59
66
|
*/
|
|
60
67
|
beforeRetry?: FetcherBeforeRetryHook[]
|
|
68
|
+
|
|
69
|
+
onError?: FetcherOnErrorHook[]
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
/**
|
|
73
|
+
* If Fetcher has an error - `errorData` object will be appended to the error data.
|
|
74
|
+
* Like this:
|
|
75
|
+
*
|
|
76
|
+
* _errorDataAppend(err, cfg.errorData)
|
|
77
|
+
*
|
|
78
|
+
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
|
|
79
|
+
*/
|
|
80
|
+
errorData?: ErrorData | undefined
|
|
81
|
+
|
|
63
82
|
/**
|
|
64
83
|
* If true - enables all possible logging.
|
|
65
84
|
*/
|
|
@@ -241,6 +260,22 @@ export interface FetcherOptions {
|
|
|
241
260
|
* Set to false to not throw on `!Response.ok`, but simply return `Response.body` as-is (json parsed, etc).
|
|
242
261
|
*/
|
|
243
262
|
throwHttpErrors?: boolean
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* If Fetcher has an error - `errorData` object will be appended to the error data.
|
|
266
|
+
* Like this:
|
|
267
|
+
*
|
|
268
|
+
* _errorDataAppend(err, cfg.errorData)
|
|
269
|
+
*
|
|
270
|
+
* So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
|
|
271
|
+
*/
|
|
272
|
+
errorData?: ErrorData
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Allows to mutate the error.
|
|
276
|
+
* Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
|
|
277
|
+
*/
|
|
278
|
+
onError?: FetcherOnErrorHook
|
|
244
279
|
}
|
|
245
280
|
|
|
246
281
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
package/src/http/fetcher.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ErrorLike, ErrorObject } from '../error/error.model'
|
|
|
8
8
|
import {
|
|
9
9
|
_anyToError,
|
|
10
10
|
_anyToErrorObject,
|
|
11
|
+
_errorDataAppend,
|
|
11
12
|
_errorLikeToErrorObject,
|
|
12
13
|
HttpRequestError,
|
|
13
14
|
TimeoutError,
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
FetcherBeforeRetryHook,
|
|
35
36
|
FetcherCfg,
|
|
36
37
|
FetcherNormalizedCfg,
|
|
38
|
+
FetcherOnErrorHook,
|
|
37
39
|
FetcherOptions,
|
|
38
40
|
FetcherRequest,
|
|
39
41
|
FetcherResponse,
|
|
@@ -135,6 +137,11 @@ export class Fetcher {
|
|
|
135
137
|
return this
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
onError(hook: FetcherOnErrorHook): this {
|
|
141
|
+
;(this.cfg.hooks.onError ||= []).push(hook)
|
|
142
|
+
return this
|
|
143
|
+
}
|
|
144
|
+
|
|
138
145
|
cfg: FetcherNormalizedCfg
|
|
139
146
|
|
|
140
147
|
static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
|
|
@@ -273,14 +280,14 @@ export class Fetcher {
|
|
|
273
280
|
}, timeoutSeconds * 1000) as any as number
|
|
274
281
|
}
|
|
275
282
|
|
|
276
|
-
if (
|
|
283
|
+
if (req.logRequest) {
|
|
277
284
|
const { retryAttempt } = res.retryStatus
|
|
278
285
|
logger.log(
|
|
279
286
|
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
280
287
|
.filter(Boolean)
|
|
281
288
|
.join(' '),
|
|
282
289
|
)
|
|
283
|
-
if (
|
|
290
|
+
if (req.logRequestBody && req.init.body) {
|
|
284
291
|
logger.log(req.init.body) // todo: check if we can _inspect it
|
|
285
292
|
}
|
|
286
293
|
}
|
|
@@ -335,6 +342,16 @@ export class Fetcher {
|
|
|
335
342
|
}
|
|
336
343
|
}
|
|
337
344
|
|
|
345
|
+
if (res.err) {
|
|
346
|
+
_errorDataAppend(res.err, req.errorData)
|
|
347
|
+
|
|
348
|
+
req.onError?.(res.err)
|
|
349
|
+
|
|
350
|
+
for (const hook of this.cfg.hooks.onError || []) {
|
|
351
|
+
await hook(res.err)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
338
355
|
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
339
356
|
await hook(res)
|
|
340
357
|
}
|
|
@@ -384,7 +401,7 @@ export class Fetcher {
|
|
|
384
401
|
res.retryStatus.retryStopped = true
|
|
385
402
|
|
|
386
403
|
// res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
|
|
387
|
-
if ((!res.err || !req.throwHttpErrors) &&
|
|
404
|
+
if ((!res.err || !req.throwHttpErrors) && req.logResponse) {
|
|
388
405
|
const { retryAttempt } = res.retryStatus
|
|
389
406
|
const { logger } = this.cfg
|
|
390
407
|
logger.log(
|
|
@@ -399,7 +416,7 @@ export class Fetcher {
|
|
|
399
416
|
.join(' '),
|
|
400
417
|
)
|
|
401
418
|
|
|
402
|
-
if (
|
|
419
|
+
if (req.logResponseBody && res.body !== undefined) {
|
|
403
420
|
logger.log(res.body)
|
|
404
421
|
}
|
|
405
422
|
}
|
|
@@ -664,6 +681,7 @@ export class Fetcher {
|
|
|
664
681
|
},
|
|
665
682
|
hooks: {},
|
|
666
683
|
throwHttpErrors: true,
|
|
684
|
+
errorData: {},
|
|
667
685
|
},
|
|
668
686
|
_omit(cfg, ['method', 'credentials', 'headers', 'redirect', 'logger']),
|
|
669
687
|
)
|
|
@@ -688,6 +706,7 @@ export class Fetcher {
|
|
|
688
706
|
'logResponseBody',
|
|
689
707
|
'debug',
|
|
690
708
|
'throwHttpErrors',
|
|
709
|
+
'errorData',
|
|
691
710
|
]),
|
|
692
711
|
started: Date.now(),
|
|
693
712
|
..._omit(opt, ['method', 'headers', 'credentials']),
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export * from './string/pupa'
|
|
|
74
74
|
export * from './string/readingTime'
|
|
75
75
|
export * from './string/regex'
|
|
76
76
|
export * from './string/safeJsonStringify'
|
|
77
|
+
export * from './string/slugify'
|
|
77
78
|
export * from './string/string.util'
|
|
78
79
|
export * from './string/stringify'
|
|
79
80
|
export * from './string/url.util'
|
|
@@ -57,6 +57,7 @@ export function _isBetween<T extends number | string>(
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function _clamp(x: number, minIncl: number, maxIncl: number): number {
|
|
60
|
+
// eslint-disable-next-line unicorn/prefer-math-min-max
|
|
60
61
|
return x <= minIncl ? minIncl : x >= maxIncl ? maxIncl : x
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -400,6 +400,7 @@ type PropertyPath = Many<PropertyKey>
|
|
|
400
400
|
* Based on: https://stackoverflow.com/a/54733755/4919972
|
|
401
401
|
*/
|
|
402
402
|
export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any): T {
|
|
403
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
403
404
|
if (!obj || Object(obj) !== obj || !path) return obj as any // When obj is not an object
|
|
404
405
|
|
|
405
406
|
// If not yet an array, get the keys from the string-path
|
|
@@ -416,6 +417,7 @@ export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any
|
|
|
416
417
|
c,
|
|
417
418
|
i, // Iterate all of them except the last one
|
|
418
419
|
) =>
|
|
420
|
+
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
419
421
|
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
420
422
|
? // Yes: then follow that path
|
|
421
423
|
a[c]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Credit to (adopted from): https://github.com/sindresorhus/slugify/
|
|
2
|
+
|
|
3
|
+
export interface SlugifyOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Default: `-`
|
|
6
|
+
*/
|
|
7
|
+
separator?: string
|
|
8
|
+
/**
|
|
9
|
+
* Default: true
|
|
10
|
+
*/
|
|
11
|
+
lowercase?: boolean
|
|
12
|
+
/**
|
|
13
|
+
* Default: true
|
|
14
|
+
*/
|
|
15
|
+
decamelize?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Default: []
|
|
18
|
+
*/
|
|
19
|
+
preserveCharacters?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function _slugify(s: string, opt: SlugifyOptions = {}): string {
|
|
23
|
+
opt = {
|
|
24
|
+
separator: '-',
|
|
25
|
+
lowercase: true,
|
|
26
|
+
decamelize: true,
|
|
27
|
+
preserveCharacters: [],
|
|
28
|
+
...opt,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (opt.decamelize) {
|
|
32
|
+
s = decamelize(s)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const patternSlug = buildPatternSlug(opt)
|
|
36
|
+
|
|
37
|
+
if (opt.lowercase) {
|
|
38
|
+
s = s.toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// based on https://stackoverflow.com/a/23633850/4919972
|
|
42
|
+
// Combining Diacritical Marks
|
|
43
|
+
// https://www.unicode.org/charts/PDF/U0300.pdf
|
|
44
|
+
// biome-ignore lint/suspicious/noMisleadingCharacterClass: ok
|
|
45
|
+
s = s.normalize('NFKD').replaceAll(/[\u0300-\u036F]/g, '')
|
|
46
|
+
|
|
47
|
+
// Detect contractions/possessives by looking for any word followed by a `'t`
|
|
48
|
+
// or `'s` in isolation and then remove it.
|
|
49
|
+
s = s.replaceAll(/([a-zA-Z\d]+)'([ts])(\s|$)/g, '$1$2$3')
|
|
50
|
+
|
|
51
|
+
s = s.replace(patternSlug, opt.separator!)
|
|
52
|
+
s = s.replaceAll('\\', '')
|
|
53
|
+
|
|
54
|
+
if (opt.separator) {
|
|
55
|
+
s = removeMootSeparators(s, opt.separator)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return s
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildPatternSlug(options: any): RegExp {
|
|
62
|
+
let negationSetPattern = String.raw`a-z\d`
|
|
63
|
+
negationSetPattern += options.lowercase ? '' : 'A-Z'
|
|
64
|
+
|
|
65
|
+
if (options.preserveCharacters.length > 0) {
|
|
66
|
+
for (const character of options.preserveCharacters) {
|
|
67
|
+
if (character === options.separator) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`The separator character \`${options.separator}\` cannot be included in preserved characters: ${options.preserveCharacters}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
negationSetPattern += escapeStringRegexp(character)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new RegExp(`[^${negationSetPattern}]+`, 'g')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeMootSeparators(s: string, separator: string): string {
|
|
81
|
+
const escapedSeparator = escapeStringRegexp(separator)
|
|
82
|
+
|
|
83
|
+
return s
|
|
84
|
+
.replaceAll(new RegExp(`${escapedSeparator}{2,}`, 'g'), separator)
|
|
85
|
+
.replaceAll(new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'), '')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function decamelize(s: string): string {
|
|
89
|
+
return (
|
|
90
|
+
s
|
|
91
|
+
// Separate capitalized words.
|
|
92
|
+
.replaceAll(/([A-Z]{2,})(\d+)/g, '$1 $2')
|
|
93
|
+
.replaceAll(/([a-z\d]+)([A-Z]{2,})/g, '$1 $2')
|
|
94
|
+
|
|
95
|
+
.replaceAll(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
96
|
+
// `[a-rt-z]` matches all lowercase characters except `s`.
|
|
97
|
+
// This avoids matching plural acronyms like `APIs`.
|
|
98
|
+
.replaceAll(/([A-Z]+)([A-Z][a-rt-z\d]+)/g, '$1 $2')
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// based on: https://github.com/sindresorhus/escape-string-regexp/
|
|
103
|
+
function escapeStringRegexp(s: string): string {
|
|
104
|
+
// Escape characters with special meaning either inside or outside character sets.
|
|
105
|
+
// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
|
|
106
|
+
return s.replaceAll(/[|\\{}()[\]^$+*?.]/g, String.raw`\$&`).replaceAll('-', String.raw`\x2d`)
|
|
107
|
+
}
|