@naturalcycles/js-lib 14.252.0 → 14.253.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.
@@ -1,5 +1,4 @@
1
- import type { ErrorData, ErrorObject } from '..';
2
- import { Class } from '..';
1
+ import { BackendErrorResponseObject, Class, ErrorData, ErrorObject } from '..';
3
2
  /**
4
3
  * Evaluates the `condition` (casts it to Boolean).
5
4
  * Expects it to be truthy, otherwise throws AppError.
@@ -40,6 +39,7 @@ export declare function _assertIsError<ERR extends Error = Error>(err: any, erro
40
39
  */
41
40
  export declare function _assertErrorClassOrRethrow<ERR extends Error>(err: any, errorClass: Class<ERR>): asserts err is ERR;
42
41
  export declare function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(obj: any): asserts obj is ErrorObject<DATA_TYPE>;
42
+ export declare function _assertIsBackendErrorResponseObject<DATA_TYPE extends ErrorData = ErrorData>(obj: any): asserts obj is BackendErrorResponseObject<DATA_TYPE>;
43
43
  export declare function _assertIsString(v: any, message?: string): asserts v is string;
44
44
  export declare function _assertIsNumber(v: any, message?: string): asserts v is number;
45
45
  export declare function _assertTypeOf<T>(v: any, expectedType: string, message?: string): asserts v is T;
@@ -6,6 +6,7 @@ exports._assertDeepEquals = _assertDeepEquals;
6
6
  exports._assertIsError = _assertIsError;
7
7
  exports._assertErrorClassOrRethrow = _assertErrorClassOrRethrow;
8
8
  exports._assertIsErrorObject = _assertIsErrorObject;
9
+ exports._assertIsBackendErrorResponseObject = _assertIsBackendErrorResponseObject;
9
10
  exports._assertIsString = _assertIsString;
10
11
  exports._assertIsNumber = _assertIsNumber;
11
12
  exports._assertTypeOf = _assertTypeOf;
@@ -89,6 +90,11 @@ function _assertIsErrorObject(obj) {
89
90
  throw new __1.AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`);
90
91
  }
91
92
  }
93
+ function _assertIsBackendErrorResponseObject(obj) {
94
+ if (!(0, __1._isBackendErrorResponseObject)(obj)) {
95
+ throw new __1.AssertionError(`Expected to be BackendErrorResponseObject, actual typeof: ${typeof obj}`);
96
+ }
97
+ }
92
98
  function _assertIsString(v, message) {
93
99
  _assertTypeOf(v, 'string', message);
94
100
  }
@@ -320,23 +320,32 @@ class Fetcher {
320
320
  }
321
321
  async onNotOkResponse(res) {
322
322
  let cause;
323
+ // Try to fetch body and attach to res.body
324
+ // (but don't fail if it doesn't work)
325
+ if (!res.body && res.fetchResponse) {
326
+ try {
327
+ res.body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
328
+ }
329
+ catch {
330
+ // ignore body fetching/parsing errors at this point
331
+ }
332
+ }
323
333
  if (res.err) {
324
334
  // This is only possible on JSON.parse error, or CORS error,
325
335
  // or `unexpected redirect`
326
336
  // This check should go first, to avoid calling .text() twice (which will fail)
327
337
  cause = (0, error_util_1._errorLikeToErrorObject)(res.err);
328
338
  }
329
- else if (res.fetchResponse) {
330
- const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
331
- if (body) {
332
- cause = (0, error_util_1._anyToErrorObject)(body);
333
- }
339
+ else if (res.body) {
340
+ cause = (0, error_util_1._anyToErrorObject)(res.body);
341
+ }
342
+ else {
343
+ cause = {
344
+ name: 'Error',
345
+ message: 'Fetch failed',
346
+ data: {},
347
+ };
334
348
  }
335
- cause ||= {
336
- name: 'Error',
337
- message: 'Fetch failed',
338
- data: {},
339
- };
340
349
  let responseStatusCode = res.fetchResponse?.status || 0;
341
350
  if (res.statusFamily === 2) {
342
351
  // important to reset responseStatusCode to 0 in this case, as status 2xx can be misleading
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);
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { _deepEquals, _isErrorObject, _stringify, AssertionError } from '..';
1
+ import { _deepEquals, _isBackendErrorResponseObject, _isErrorObject, _stringify, AssertionError, } from '..';
2
2
  /**
3
3
  * Evaluates the `condition` (casts it to Boolean).
4
4
  * Expects it to be truthy, otherwise throws AppError.
@@ -78,6 +78,11 @@ export function _assertIsErrorObject(obj) {
78
78
  throw new AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`);
79
79
  }
80
80
  }
81
+ export function _assertIsBackendErrorResponseObject(obj) {
82
+ if (!_isBackendErrorResponseObject(obj)) {
83
+ throw new AssertionError(`Expected to be BackendErrorResponseObject, actual typeof: ${typeof obj}`);
84
+ }
85
+ }
81
86
  export function _assertIsString(v, message) {
82
87
  _assertTypeOf(v, 'string', message);
83
88
  }
@@ -312,23 +312,32 @@ export class Fetcher {
312
312
  }
313
313
  async onNotOkResponse(res) {
314
314
  let cause;
315
+ // Try to fetch body and attach to res.body
316
+ // (but don't fail if it doesn't work)
317
+ if (!res.body && res.fetchResponse) {
318
+ try {
319
+ res.body = _jsonParseIfPossible(await res.fetchResponse.text());
320
+ }
321
+ catch {
322
+ // ignore body fetching/parsing errors at this point
323
+ }
324
+ }
315
325
  if (res.err) {
316
326
  // This is only possible on JSON.parse error, or CORS error,
317
327
  // or `unexpected redirect`
318
328
  // This check should go first, to avoid calling .text() twice (which will fail)
319
329
  cause = _errorLikeToErrorObject(res.err);
320
330
  }
321
- else if (res.fetchResponse) {
322
- const body = _jsonParseIfPossible(await res.fetchResponse.text());
323
- if (body) {
324
- cause = _anyToErrorObject(body);
325
- }
331
+ else if (res.body) {
332
+ cause = _anyToErrorObject(res.body);
333
+ }
334
+ else {
335
+ cause = {
336
+ name: 'Error',
337
+ message: 'Fetch failed',
338
+ data: {},
339
+ };
326
340
  }
327
- cause || (cause = {
328
- name: 'Error',
329
- message: 'Fetch failed',
330
- data: {},
331
- });
332
341
  let responseStatusCode = res.fetchResponse?.status || 0;
333
342
  if (res.statusFamily === 2) {
334
343
  // important to reset responseStatusCode to 0 in this case, as status 2xx can be misleading
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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.252.0",
3
+ "version": "14.253.0",
4
4
  "scripts": {
5
5
  "prepare": "husky",
6
6
  "build": "dev-lib build-esm-cjs",
@@ -1,5 +1,14 @@
1
- import type { ErrorData, ErrorObject } from '..'
2
- import { _deepEquals, _isErrorObject, _stringify, AssertionError, Class } from '..'
1
+ import {
2
+ _deepEquals,
3
+ _isBackendErrorResponseObject,
4
+ _isErrorObject,
5
+ _stringify,
6
+ AssertionError,
7
+ BackendErrorResponseObject,
8
+ Class,
9
+ ErrorData,
10
+ ErrorObject,
11
+ } from '..'
3
12
 
4
13
  /**
5
14
  * Evaluates the `condition` (casts it to Boolean).
@@ -113,6 +122,16 @@ export function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
113
122
  }
114
123
  }
115
124
 
125
+ export function _assertIsBackendErrorResponseObject<DATA_TYPE extends ErrorData = ErrorData>(
126
+ obj: any,
127
+ ): asserts obj is BackendErrorResponseObject<DATA_TYPE> {
128
+ if (!_isBackendErrorResponseObject(obj)) {
129
+ throw new AssertionError(
130
+ `Expected to be BackendErrorResponseObject, actual typeof: ${typeof obj}`,
131
+ )
132
+ }
133
+ }
134
+
116
135
  export function _assertIsString(v: any, message?: string): asserts v is string {
117
136
  _assertTypeOf<string>(v, 'string', message)
118
137
  }
@@ -416,24 +416,31 @@ export class Fetcher {
416
416
  private async onNotOkResponse(res: FetcherResponse): Promise<void> {
417
417
  let cause: ErrorObject
418
418
 
419
+ // Try to fetch body and attach to res.body
420
+ // (but don't fail if it doesn't work)
421
+ if (!res.body && res.fetchResponse) {
422
+ try {
423
+ res.body = _jsonParseIfPossible(await res.fetchResponse.text())
424
+ } catch {
425
+ // ignore body fetching/parsing errors at this point
426
+ }
427
+ }
428
+
419
429
  if (res.err) {
420
430
  // This is only possible on JSON.parse error, or CORS error,
421
431
  // or `unexpected redirect`
422
432
  // This check should go first, to avoid calling .text() twice (which will fail)
423
433
  cause = _errorLikeToErrorObject(res.err)
424
- } else if (res.fetchResponse) {
425
- const body = _jsonParseIfPossible(await res.fetchResponse.text())
426
- if (body) {
427
- cause = _anyToErrorObject(body)
434
+ } else if (res.body) {
435
+ cause = _anyToErrorObject(res.body)
436
+ } else {
437
+ cause = {
438
+ name: 'Error',
439
+ message: 'Fetch failed',
440
+ data: {},
428
441
  }
429
442
  }
430
443
 
431
- cause ||= {
432
- name: 'Error',
433
- message: 'Fetch failed',
434
- data: {},
435
- }
436
-
437
444
  let responseStatusCode = res.fetchResponse?.status || 0
438
445
  if (res.statusFamily === 2) {
439
446
  // important to reset responseStatusCode to 0 in this case, as status 2xx can be misleading
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'
@@ -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
+ }