@codeleap/utils 6.3.0 → 6.8.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/array.d.ts +33 -0
- package/dist/array.d.ts.map +1 -0
- package/dist/cloneDeep.d.ts +10 -0
- package/dist/cloneDeep.d.ts.map +1 -0
- package/dist/colors.d.ts +22 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/date.d.ts +13 -0
- package/dist/date.d.ts.map +1 -0
- package/dist/faker.d.ts +23 -0
- package/dist/faker.d.ts.map +1 -0
- package/dist/file.d.ts +14 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/locale.d.ts +10 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/misc.d.ts +67 -0
- package/dist/misc.d.ts.map +1 -0
- package/dist/object.d.ts +115 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/react.d.ts +118 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/string.d.ts +43 -0
- package/dist/string.d.ts.map +1 -0
- package/package.json +22 -8
- package/src/array.ts +28 -3
- package/src/cloneDeep.ts +17 -9
- package/src/colors.ts +19 -3
- package/src/date.ts +9 -0
- package/src/faker.ts +13 -0
- package/src/file.ts +8 -0
- package/src/locale.ts +7 -0
- package/src/misc.ts +56 -8
- package/src/object.ts +122 -20
- package/src/react.tsx +51 -16
- package/src/string.ts +37 -2
- package/package.json.bak +0 -31
package/src/array.ts
CHANGED
|
@@ -2,19 +2,26 @@ import { FunctionType } from '@codeleap/types'
|
|
|
2
2
|
|
|
3
3
|
type GetterFunction<T> = FunctionType<[T, number], string | number> | keyof T
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Indexes an array into an object keyed by a derived value.
|
|
7
|
+
*
|
|
8
|
+
* `keyAccessor` may be a property name string or a callback that returns a
|
|
9
|
+
* key. When omitted, array indices are used as keys. Duplicate keys silently
|
|
10
|
+
* overwrite earlier entries.
|
|
11
|
+
*/
|
|
5
12
|
export function objectFromArray<T, Getter extends GetterFunction<T>>(
|
|
6
13
|
arr: T[],
|
|
7
14
|
keyAccessor?: Getter,
|
|
8
15
|
): Record<string, T> {
|
|
9
|
-
let getObjectKey = (_, idx) => idx
|
|
16
|
+
let getObjectKey: (value: T, idx: number) => string | number = (_, idx) => idx
|
|
10
17
|
|
|
11
18
|
if (keyAccessor) {
|
|
12
19
|
switch (typeof keyAccessor) {
|
|
13
20
|
case 'string':
|
|
14
|
-
getObjectKey = (value) => value[keyAccessor]
|
|
21
|
+
getObjectKey = (value: T) => value[keyAccessor as keyof T] as unknown as string | number
|
|
15
22
|
break
|
|
16
23
|
case 'function':
|
|
17
|
-
getObjectKey = keyAccessor
|
|
24
|
+
getObjectKey = keyAccessor as (value: T, idx: number) => string | number
|
|
18
25
|
break
|
|
19
26
|
}
|
|
20
27
|
}
|
|
@@ -24,6 +31,13 @@ export function objectFromArray<T, Getter extends GetterFunction<T>>(
|
|
|
24
31
|
return Object.fromEntries(indexedMap)
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Returns a deduplicated copy of `array` where uniqueness is determined by
|
|
36
|
+
* the value returned from `getProperty`.
|
|
37
|
+
*
|
|
38
|
+
* When duplicates exist, the **last** occurrence wins because `objectFromArray`
|
|
39
|
+
* overwrites earlier entries with the same key.
|
|
40
|
+
*/
|
|
27
41
|
export function uniqueArrayByProperty<T, G extends GetterFunction<T>>(
|
|
28
42
|
array: T[],
|
|
29
43
|
getProperty: G,
|
|
@@ -31,6 +45,12 @@ export function uniqueArrayByProperty<T, G extends GetterFunction<T>>(
|
|
|
31
45
|
return Object.values(objectFromArray(array, getProperty))
|
|
32
46
|
}
|
|
33
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Recursively flattens a nested array of arbitrary depth into a single-level array.
|
|
50
|
+
*
|
|
51
|
+
* Unlike `Array.prototype.flat(Infinity)`, this implementation does not rely on
|
|
52
|
+
* native flat support and works identically across all environments.
|
|
53
|
+
*/
|
|
34
54
|
export function flatten<T extends unknown>(arr: T[]) {
|
|
35
55
|
let newArr = [] as T[]
|
|
36
56
|
|
|
@@ -45,6 +65,11 @@ export function flatten<T extends unknown>(arr: T[]) {
|
|
|
45
65
|
return newArr
|
|
46
66
|
}
|
|
47
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Returns an inclusive integer array from `start` to `end`.
|
|
70
|
+
*
|
|
71
|
+
* Both bounds are included: `range(1, 3)` → `[1, 2, 3]`.
|
|
72
|
+
*/
|
|
48
73
|
export function range(start: number, end: number) {
|
|
49
74
|
const length = end - start + 1
|
|
50
75
|
return Array.from({ length }, (_, index) => index + start)
|
package/src/cloneDeep.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Produces a full recursive clone of `value` without any external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Supported types: primitives (returned as-is), plain objects, arrays, `Date`,
|
|
5
|
+
* `Map`, `Set`, and `RegExp`. Class instances with prototype methods beyond
|
|
6
|
+
* these built-ins are cloned as plain objects — prototype chain is not preserved.
|
|
7
|
+
* Circular references will cause a stack overflow.
|
|
8
|
+
*/
|
|
9
|
+
export function cloneDeep<T = any>(value: T): T {
|
|
2
10
|
if (value === null || typeof value !== 'object') {
|
|
3
11
|
return value
|
|
4
12
|
}
|
|
5
13
|
|
|
6
14
|
if (Array.isArray(value)) {
|
|
7
|
-
return value.map(cloneDeep)
|
|
15
|
+
return value.map(cloneDeep) as unknown as T
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
if (value instanceof Date) {
|
|
11
|
-
return new Date(value.getTime())
|
|
19
|
+
return new Date(value.getTime()) as unknown as T
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
if (value instanceof Map) {
|
|
@@ -16,7 +24,7 @@ export function cloneDeep(value) {
|
|
|
16
24
|
value.forEach((v, k) => {
|
|
17
25
|
clonedMap.set(k, cloneDeep(v))
|
|
18
26
|
})
|
|
19
|
-
return clonedMap
|
|
27
|
+
return clonedMap as unknown as T
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
if (value instanceof Set) {
|
|
@@ -24,20 +32,20 @@ export function cloneDeep(value) {
|
|
|
24
32
|
value.forEach((v) => {
|
|
25
33
|
clonedSet.add(cloneDeep(v))
|
|
26
34
|
})
|
|
27
|
-
return clonedSet
|
|
35
|
+
return clonedSet as unknown as T
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
if (value instanceof RegExp) {
|
|
31
|
-
return new RegExp(value.source, value.flags)
|
|
39
|
+
return new RegExp(value.source, value.flags) as unknown as T
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
const clonedObj = {}
|
|
42
|
+
const clonedObj: Record<string, unknown> = {}
|
|
35
43
|
|
|
36
44
|
for (const key in value) {
|
|
37
45
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
38
|
-
clonedObj[key] = cloneDeep(value[key])
|
|
46
|
+
clonedObj[key] = cloneDeep((value as Record<string, unknown>)[key])
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
return clonedObj
|
|
50
|
+
return clonedObj as unknown as T
|
|
43
51
|
}
|
package/src/colors.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import tinycolor from 'tinycolor2'
|
|
2
2
|
import { TypeGuards } from '@codeleap/types'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Parses a 6-digit hex colour string (with or without leading `#`) into its
|
|
6
|
+
* `{ r, g, b }` components.
|
|
7
|
+
*
|
|
8
|
+
* Returns `null` for invalid or short (3-digit) hex values.
|
|
9
|
+
*/
|
|
10
|
+
export function hexToRgb(hex: string) {
|
|
5
11
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
6
12
|
return result
|
|
7
13
|
? {
|
|
@@ -12,8 +18,18 @@ export function hexToRgb(hex) {
|
|
|
12
18
|
: null
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
const shadeColorCache = {}
|
|
16
|
-
|
|
21
|
+
const shadeColorCache: Record<string, string> = {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lightens or darkens `color` by `percent` and optionally applies `opacity`,
|
|
25
|
+
* returning an `rgba(...)` string.
|
|
26
|
+
*
|
|
27
|
+
* - Positive `percent` → lighten; negative → darken (magnitude is used).
|
|
28
|
+
* - `opacity` must be in the `0–1` range (passed directly to tinycolor's `setAlpha`).
|
|
29
|
+
* - Results are memoised in a module-level cache keyed by the serialised parameters,
|
|
30
|
+
* so repeated calls with identical arguments are effectively free.
|
|
31
|
+
*/
|
|
32
|
+
export function shadeColor(color: string, percent = 0, opacity: number | null = null) {
|
|
17
33
|
const _color = color.trim()
|
|
18
34
|
const serialParams = [_color, percent.toString()]
|
|
19
35
|
if (TypeGuards.isNumber(opacity)) {
|
package/src/date.ts
CHANGED
|
@@ -14,6 +14,15 @@ const removeTimezoneAndFormat = (date: any, format = 'YYYY-MM-DD') => {
|
|
|
14
14
|
return dayjs(normalizedDate).startOf('day').format(format)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Date helpers that normalise timezone-sensitive values before formatting.
|
|
19
|
+
*
|
|
20
|
+
* - `removeTimezoneAndFormat(date, format?)` — strips the time component from
|
|
21
|
+
* a `Date` or ISO string, anchors it to noon local time, then formats with
|
|
22
|
+
* dayjs. This prevents off-by-one-day errors that occur when UTC midnight
|
|
23
|
+
* falls on a different calendar day in the user's timezone. Returns `''` for
|
|
24
|
+
* falsy input.
|
|
25
|
+
*/
|
|
17
26
|
export const dateUtils = {
|
|
18
27
|
removeTimezoneAndFormat,
|
|
19
28
|
}
|
package/src/faker.ts
CHANGED
|
@@ -25,6 +25,19 @@ function number(min: number = 0, max: number = 100) {
|
|
|
25
25
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Lightweight in-house fixture-data generator with no external dependencies.
|
|
30
|
+
*
|
|
31
|
+
* Provided methods:
|
|
32
|
+
* - `firstName()` / `lastName()` — random entries from hard-coded name lists.
|
|
33
|
+
* - `name()` — space-joined first + last name.
|
|
34
|
+
* - `animal()` — random animal name.
|
|
35
|
+
* - `number(min?, max?)` — random integer in `[min, max]` (default 0–100).
|
|
36
|
+
*
|
|
37
|
+
* All selections use `Math.random`, so results are not reproducible across calls.
|
|
38
|
+
* Use this instead of heavy faker libraries in tests or seed scripts where the
|
|
39
|
+
* small vocabulary is sufficient.
|
|
40
|
+
*/
|
|
28
41
|
export const faker = {
|
|
29
42
|
lastName: () => getRandom(surnames),
|
|
30
43
|
firstName: () => getRandom(names),
|
package/src/file.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const separators = /[\\\/]+/
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Splits a file system path into its directory, base name, and extension.
|
|
5
|
+
*
|
|
6
|
+
* Both `/` and `\` are treated as separators, so Windows and POSIX paths are
|
|
7
|
+
* handled uniformly. Files without an extension return an empty string for
|
|
8
|
+
* `extension`. The returned `path` is always joined with `/` regardless of the
|
|
9
|
+
* original separator.
|
|
10
|
+
*/
|
|
3
11
|
export function parseFilePathData(path: string) {
|
|
4
12
|
const parts = path.split(separators)
|
|
5
13
|
|
package/src/locale.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AnyRecord } from '@codeleap/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Finds the closest matching key in `languageDictionary` for a given `locale`.
|
|
5
|
+
*
|
|
6
|
+
* Matching uses only the first two characters (the language subtag), so `'en-AU'`
|
|
7
|
+
* will match an `'en-US'` dictionary key. Returns `defaultLocale` when no
|
|
8
|
+
* partial match exists.
|
|
9
|
+
*/
|
|
3
10
|
export function getSimilarLocale(
|
|
4
11
|
locale: string,
|
|
5
12
|
defaultLocale: string,
|
package/src/misc.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { capitalize } from './string'
|
|
2
2
|
import { StylesOf } from '@codeleap/types'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Converts a remote image URL into a file-upload-compatible object
|
|
6
|
+
* (`{ uri, name, type }`).
|
|
7
|
+
*
|
|
8
|
+
* The `type` is inferred from the URL's file extension. Returns `null` when
|
|
9
|
+
* `imagePath` is falsy, making it safe to pass directly to optional file-input APIs.
|
|
10
|
+
*/
|
|
4
11
|
export function imagePathToFileObject(imagePath: string | null) {
|
|
5
12
|
const parts = imagePath ? imagePath.split('.') : ''
|
|
6
13
|
|
|
@@ -17,7 +24,7 @@ export function imagePathToFileObject(imagePath: string | null) {
|
|
|
17
24
|
return fileValue
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
const letterToColorMap = {
|
|
27
|
+
const letterToColorMap: Record<string, string> = {
|
|
21
28
|
a: '#7CB9E8',
|
|
22
29
|
b: '#3a9e77',
|
|
23
30
|
c: '#A3C1AD',
|
|
@@ -46,12 +53,20 @@ const letterToColorMap = {
|
|
|
46
53
|
z: '#ff8295',
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Maps the first character of `anyString` to a deterministic colour for use in
|
|
58
|
+
* avatar placeholders.
|
|
59
|
+
*
|
|
60
|
+
* Lookup is case-insensitive. Returns `'#999999'` for empty, undefined, or
|
|
61
|
+
* characters not in the `a–z` range.
|
|
62
|
+
*/
|
|
49
63
|
export function matchInitialToColor(anyString?: string) {
|
|
50
64
|
if (!anyString) return '#999999'
|
|
51
65
|
return letterToColorMap[anyString.toLowerCase().charAt(0)] || '#999999'
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
/** Returns a Promise that resolves after `ms` milliseconds. */
|
|
69
|
+
export function waitFor(ms: number) {
|
|
55
70
|
return new Promise<void>((resolve) => {
|
|
56
71
|
setTimeout(() => {
|
|
57
72
|
resolve()
|
|
@@ -64,15 +79,23 @@ type ParseSourceUrlArg = {
|
|
|
64
79
|
src?: string
|
|
65
80
|
}
|
|
66
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Resolves a media URL from either a raw string or a `{ source?, src? }` object.
|
|
84
|
+
*
|
|
85
|
+
* - Paths starting with `/media/` are prefixed with `Settings.BaseURL`.
|
|
86
|
+
* - Absolute URLs are returned unchanged.
|
|
87
|
+
* - Empty/missing addresses fall back to a random `picsum.photos` placeholder.
|
|
88
|
+
* - Returns `null` for a falsy `args` argument.
|
|
89
|
+
*/
|
|
67
90
|
export function parseSourceUrl(args: string, Settings?: any): string
|
|
68
91
|
export function parseSourceUrl(
|
|
69
92
|
args: ParseSourceUrlArg,
|
|
70
93
|
Settings?: any
|
|
71
|
-
): string
|
|
94
|
+
): string | null
|
|
72
95
|
export function parseSourceUrl(
|
|
73
96
|
args: ParseSourceUrlArg | string,
|
|
74
97
|
Settings?: any,
|
|
75
|
-
): string {
|
|
98
|
+
): string | null {
|
|
76
99
|
if (!args) return null
|
|
77
100
|
|
|
78
101
|
let res = ''
|
|
@@ -94,8 +117,16 @@ export function parseSourceUrl(
|
|
|
94
117
|
return res
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Extracts style entries from a variant styles map whose keys begin with `match`,
|
|
122
|
+
* returning them as a new object with the prefix stripped and the first
|
|
123
|
+
* remaining character lowercased.
|
|
124
|
+
*
|
|
125
|
+
* Used internally by component style systems to pull out sub-part styles
|
|
126
|
+
* (e.g. `inputLabel`, `inputWrapper`) into their own namespaced objects.
|
|
127
|
+
*/
|
|
97
128
|
export function getNestedStylesByKey<T extends StylesOf<any>>(match:string, variantStyles: T) {
|
|
98
|
-
const styles = {}
|
|
129
|
+
const styles: Record<string, unknown> = {}
|
|
99
130
|
|
|
100
131
|
for (const [key, value] of Object.entries(variantStyles)) {
|
|
101
132
|
|
|
@@ -108,6 +139,15 @@ export function getNestedStylesByKey<T extends StylesOf<any>>(match:string, vari
|
|
|
108
139
|
return styles
|
|
109
140
|
}
|
|
110
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Heuristically detects whether the app has been React Fast Refreshed since
|
|
144
|
+
* initial launch.
|
|
145
|
+
*
|
|
146
|
+
* Compares the elapsed time since `Settings.Environment.InitTime` against a
|
|
147
|
+
* 1-second threshold. Fast Refresh typically completes well under 1 second from
|
|
148
|
+
* app start; anything longer suggests a subsequent refresh rather than cold boot.
|
|
149
|
+
* Returns `undefined` and logs a warning when `InitTime` is not set.
|
|
150
|
+
*/
|
|
111
151
|
export function hasFastRefreshed(Settings: any) {
|
|
112
152
|
if (Settings?.Environment?.InitTime) {
|
|
113
153
|
const timeFromStartup = (new Date()).getTime() - Settings.Environment.InitTime.getTime()
|
|
@@ -120,9 +160,17 @@ export function hasFastRefreshed(Settings: any) {
|
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
162
|
|
|
123
|
-
const throttleTimerId =
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
const throttleTimerId: Record<string | number, ReturnType<typeof setTimeout> | undefined> = {}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Leading-edge throttle keyed by a `ref` identifier rather than by function reference.
|
|
167
|
+
*
|
|
168
|
+
* `func` is called immediately on the first invocation for a given `ref`; subsequent
|
|
169
|
+
* calls with the same `ref` are dropped until `delay` milliseconds have elapsed.
|
|
170
|
+
* Using a string/number `ref` allows multiple independent throttle timers to coexist
|
|
171
|
+
* in a single module without needing to hold separate timer handles.
|
|
172
|
+
*/
|
|
173
|
+
export function throttle(func: () => void, ref: string | number, delay: number) {
|
|
126
174
|
if (throttleTimerId[ref]) {
|
|
127
175
|
return
|
|
128
176
|
}
|
package/src/object.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { FunctionType, AppSettings } from '@codeleap/types'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Recursively merges `changes` into `base`, deep-merging nested plain objects.
|
|
5
|
+
*
|
|
6
|
+
* Arrays and `Date` instances are treated as scalar values — they replace rather
|
|
7
|
+
* than merge. If `changes` is not iterable, it is returned as-is.
|
|
8
|
+
*/
|
|
9
|
+
export function deepMerge(base: Record<string, any> = {}, changes: Record<string, any> = {}): any {
|
|
10
|
+
const obj: Record<string, any> = {
|
|
5
11
|
...base,
|
|
6
12
|
}
|
|
7
|
-
let changeEntries = []
|
|
13
|
+
let changeEntries: [string, any][] = []
|
|
8
14
|
try {
|
|
9
15
|
changeEntries = Object.entries(changes)
|
|
10
16
|
} catch (e) {
|
|
@@ -21,30 +27,46 @@ export function deepMerge(base = {}, changes = {}): any {
|
|
|
21
27
|
return obj
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Like `Array.prototype.map` but over an object's entries.
|
|
32
|
+
*
|
|
33
|
+
* Returns an array — not a new object — so it is useful when you need to
|
|
34
|
+
* transform key-value pairs into arbitrary values (e.g. JSX elements).
|
|
35
|
+
*/
|
|
24
36
|
export function mapObject<T>(
|
|
25
37
|
obj: T,
|
|
26
38
|
callback: FunctionType<[[keyof T, T[keyof T]]], any>,
|
|
27
39
|
) {
|
|
28
|
-
return Object.entries(obj).map((args) => callback(args as [keyof T, T[keyof T]]),
|
|
40
|
+
return Object.entries(obj as Record<string, unknown>).map((args) => callback(args as [keyof T, T[keyof T]]),
|
|
29
41
|
)
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Sets a value at an arbitrarily deep dot-separated `path`, mutating `base` in place
|
|
46
|
+
* and returning it.
|
|
47
|
+
*
|
|
48
|
+
* Numeric path segments are coerced to array indices when the current node is an array.
|
|
49
|
+
*/
|
|
50
|
+
export const deepSet = (base: any = {}, path: string, value: any): any => {
|
|
51
|
+
const keys = path.split('.')
|
|
52
|
+
const obj = base
|
|
53
|
+
let thisKey: string | number = keys[0]
|
|
54
|
+
if (Array.isArray(base)) {
|
|
55
|
+
thisKey = Number(thisKey)
|
|
40
56
|
}
|
|
41
|
-
|
|
42
|
-
return
|
|
57
|
+
obj[thisKey] = keys.length === 1 ? value : deepSet(obj[thisKey], keys.slice(1).join('.'), value)
|
|
58
|
+
return obj
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Reads a value from `obj` at a dot-separated `path`.
|
|
63
|
+
*
|
|
64
|
+
* Returns `undefined` (without throwing) when any intermediate node is absent,
|
|
65
|
+
* because property access on `undefined` propagates silently through the loop.
|
|
66
|
+
*/
|
|
67
|
+
export const deepGet = (path: string, obj: Record<string, any>) => {
|
|
46
68
|
const parts = path.split('.')
|
|
47
|
-
let newObj = { ...obj }
|
|
69
|
+
let newObj: Record<string, any> = { ...obj }
|
|
48
70
|
|
|
49
71
|
for (const prop of parts) {
|
|
50
72
|
newObj = newObj[prop]
|
|
@@ -53,8 +75,13 @@ export const deepGet = (path, obj) => {
|
|
|
53
75
|
return newObj
|
|
54
76
|
}
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Returns every dot-separated leaf path in a nested object.
|
|
80
|
+
*
|
|
81
|
+
* Array values are treated as leaves — their indices are not traversed.
|
|
82
|
+
*/
|
|
83
|
+
export function objectPaths(obj: Record<string, any>): string[] {
|
|
84
|
+
let paths: string[] = []
|
|
58
85
|
|
|
59
86
|
Object.entries(obj).forEach(([key, value]) => {
|
|
60
87
|
if (!Array.isArray(value) && typeof value === 'object') {
|
|
@@ -67,10 +94,17 @@ export function objectPaths(obj) {
|
|
|
67
94
|
return paths
|
|
68
95
|
}
|
|
69
96
|
|
|
97
|
+
/** Returns `true` only for `string`, `number`, and `boolean` — `null`, `undefined`, and objects are excluded. */
|
|
70
98
|
export function isValuePrimitive(a:any) {
|
|
71
99
|
return ['string', 'number', 'boolean'].includes(typeof a)
|
|
72
100
|
}
|
|
73
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Inline ternary helper for spread-merging optional style or prop objects.
|
|
104
|
+
*
|
|
105
|
+
* Intended for use inside object spreads where the ternary would otherwise
|
|
106
|
+
* require wrapping: `{ ...optionalObject(flag, trueProps, {}) }`.
|
|
107
|
+
*/
|
|
74
108
|
export function optionalObject(condition: boolean, ifTrue: any, ifFalse: any) {
|
|
75
109
|
return condition ? ifTrue : ifFalse
|
|
76
110
|
}
|
|
@@ -79,7 +113,16 @@ type TraverseRecArgs = {path:string[]; value: any; depth: number; key: string; t
|
|
|
79
113
|
|
|
80
114
|
type TraverseCallback = (args?: TraverseRecArgs) => {stop?: boolean } | void
|
|
81
115
|
|
|
82
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Depth-first walk over a nested object, invoking `callback` at each node.
|
|
118
|
+
*
|
|
119
|
+
* The callback receives metadata about each visited node (`path`, `depth`,
|
|
120
|
+
* `key`, `type`, `primitive`). Primitive leaves fire the callback once; object
|
|
121
|
+
* nodes fire it for each child before recursing into it. Returning `{ stop: true }`
|
|
122
|
+
* from the callback is accepted by the type but **does not halt traversal** —
|
|
123
|
+
* the implementation does not check the return value.
|
|
124
|
+
*/
|
|
125
|
+
export function traverse(obj: any = {}, callback:TraverseCallback, args?: TraverseRecArgs) {
|
|
83
126
|
const isPrimitive = isValuePrimitive(obj)
|
|
84
127
|
|
|
85
128
|
const info = {
|
|
@@ -127,16 +170,34 @@ export function traverse(obj = {}, callback:TraverseCallback, args?: TraverseRec
|
|
|
127
170
|
}
|
|
128
171
|
}
|
|
129
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Identity helper that returns its argument unchanged, typed as `AppSettings`.
|
|
175
|
+
*
|
|
176
|
+
* Exists solely to give TypeScript a typed entry point for authoring settings
|
|
177
|
+
* objects so that editors provide autocompletion and type errors at the
|
|
178
|
+
* definition site rather than at the point of use.
|
|
179
|
+
*/
|
|
130
180
|
export function createSettings<T extends AppSettings>(a:T): T {
|
|
131
181
|
return a
|
|
132
182
|
}
|
|
133
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Returns `obj.id` if it exists, otherwise `undefined`.
|
|
186
|
+
*
|
|
187
|
+
* Intended as a default `keyExtractor` in list utilities where records are
|
|
188
|
+
* expected to carry an `id` field.
|
|
189
|
+
*/
|
|
134
190
|
export function extractKey(obj:any) {
|
|
135
191
|
if (obj.id) {
|
|
136
192
|
return obj.id
|
|
137
193
|
}
|
|
138
194
|
}
|
|
139
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Returns a new object containing only the entries for which `predicate` returns `true`.
|
|
198
|
+
*
|
|
199
|
+
* Only own enumerable keys are visited; prototype-chain properties are skipped.
|
|
200
|
+
*/
|
|
140
201
|
export function objectPickBy<K extends string = string, P = any>(obj: Record<K, P>, predicate: (valueKey: P, key: K) => boolean) {
|
|
141
202
|
const result = {} as Record<K, P>
|
|
142
203
|
|
|
@@ -149,8 +210,15 @@ export function objectPickBy<K extends string = string, P = any>(obj: Record<K,
|
|
|
149
210
|
return result
|
|
150
211
|
}
|
|
151
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Rebuilds an object by running every entry through `predicate`, which must
|
|
215
|
+
* return a `[newKey, newValue]` tuple.
|
|
216
|
+
*
|
|
217
|
+
* Keys returned by `predicate` need not be unique — later entries silently
|
|
218
|
+
* overwrite earlier ones if they resolve to the same key.
|
|
219
|
+
*/
|
|
152
220
|
export function transformObject<K extends string = string, T extends string = string>(obj: Record<K, T>, predicate: (value: T, key: K) => [K, T]): Record<string, any> {
|
|
153
|
-
const result = {}
|
|
221
|
+
const result: Record<string, any> = {}
|
|
154
222
|
|
|
155
223
|
for (const key in obj) {
|
|
156
224
|
const [newKey, newValue] = predicate?.(obj?.[key], key)
|
|
@@ -160,3 +228,37 @@ export function transformObject<K extends string = string, T extends string = st
|
|
|
160
228
|
|
|
161
229
|
return result
|
|
162
230
|
}
|
|
231
|
+
type LowercaseFirst<S extends string> = S extends `${infer F}${infer Rest}` ? `${Lowercase<F>}${Rest}` : S
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Mapped type that extracts keys of `T` starting with `P`, strips the prefix,
|
|
235
|
+
* and lowercases the first character of the remainder.
|
|
236
|
+
*
|
|
237
|
+
* Used to derive the return type of {@link filterObjectByPrefix} at compile time.
|
|
238
|
+
* For example, `FilterByPrefix<{ onPress: () => void }, 'on'>` yields `{ press: () => void }`.
|
|
239
|
+
*/
|
|
240
|
+
export type FilterByPrefix<T, P extends string> = {
|
|
241
|
+
[K in Extract<keyof T, `${P}${string}`> as K extends `${P}${infer Rest}` ? LowercaseFirst<Rest> : never]: T[K]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returns a new object containing only keys that start with `prefix`, with the
|
|
246
|
+
* prefix stripped and the first remaining character lowercased.
|
|
247
|
+
*
|
|
248
|
+
* Useful for splitting a flat props object into logical groups, e.g. separating
|
|
249
|
+
* all `inputXxx` props from a combined component interface.
|
|
250
|
+
*/
|
|
251
|
+
export function filterObjectByPrefix<T extends Record<string, any>, P extends string>(
|
|
252
|
+
obj: T,
|
|
253
|
+
prefix: P,
|
|
254
|
+
): FilterByPrefix<T, P> {
|
|
255
|
+
const result = {} as any
|
|
256
|
+
for (const key in obj) {
|
|
257
|
+
if (key.startsWith(prefix)) {
|
|
258
|
+
const newKey = key.slice(prefix.length)
|
|
259
|
+
const formattedKey = newKey.charAt(0).toLowerCase() + newKey.slice(1)
|
|
260
|
+
result[formattedKey] = obj[key]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
}
|