h3ravel-monorepo 0.1.1

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.
Files changed (176) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +42 -0
  4. package/.husky/pre-commit +1 -0
  5. package/LICENSE +21 -0
  6. package/README.md +65 -0
  7. package/docs/.gitkeep +0 -0
  8. package/eslint.config.js +17 -0
  9. package/examples/basic-app/.env.example +54 -0
  10. package/examples/basic-app/package.json +27 -0
  11. package/examples/basic-app/public/h3ravel-logo.svg +23 -0
  12. package/examples/basic-app/public/main.js +13 -0
  13. package/examples/basic-app/src/Exceptions/.gitkeep +0 -0
  14. package/examples/basic-app/src/config/app.ts +125 -0
  15. package/examples/basic-app/src/config/filesystem.ts +59 -0
  16. package/examples/basic-app/src/http/controllers/HomeController.ts +17 -0
  17. package/examples/basic-app/src/http/controllers/UserController.ts +29 -0
  18. package/examples/basic-app/src/http/middlewares/AuthMiddleware.ts +8 -0
  19. package/examples/basic-app/src/index.ts +30 -0
  20. package/examples/basic-app/src/resources/views/index.edge +383 -0
  21. package/examples/basic-app/src/routes/api.ts +17 -0
  22. package/examples/basic-app/src/routes/web.ts +6 -0
  23. package/examples/basic-app/src/storage/app/public/.gitkeep +0 -0
  24. package/examples/basic-app/tsconfig.json +8 -0
  25. package/package.json +36 -0
  26. package/packages/cache/node_modules/.bin/tsc +21 -0
  27. package/packages/cache/node_modules/.bin/tsserver +21 -0
  28. package/packages/cache/package.json +26 -0
  29. package/packages/cache/src/CacheManager.ts +1 -0
  30. package/packages/cache/src/Contracts/.gitkeep +0 -0
  31. package/packages/cache/src/Drivers/MemoryCache.ts +1 -0
  32. package/packages/cache/src/Drivers/RedisCache.ts +1 -0
  33. package/packages/cache/src/Helpers.ts +1 -0
  34. package/packages/cache/src/Providers/CacheServiceProvider.ts +16 -0
  35. package/packages/cache/src/index.ts +9 -0
  36. package/packages/cache/tests/.gitkeep +0 -0
  37. package/packages/cache/tsconfig.json +9 -0
  38. package/packages/cache/vite.config.ts +9 -0
  39. package/packages/config/node_modules/.bin/tsc +21 -0
  40. package/packages/config/node_modules/.bin/tsserver +21 -0
  41. package/packages/config/package.json +34 -0
  42. package/packages/config/src/ConfigRepository.ts +51 -0
  43. package/packages/config/src/Contracts/.gitkeep +0 -0
  44. package/packages/config/src/EnvLoader.ts +16 -0
  45. package/packages/config/src/Helpers.ts +1 -0
  46. package/packages/config/src/Providers/ConfigServiceProvider.ts +46 -0
  47. package/packages/config/src/index.ts +8 -0
  48. package/packages/config/tests/.gitkeep +0 -0
  49. package/packages/config/tsconfig.json +8 -0
  50. package/packages/config/vite.config.ts +9 -0
  51. package/packages/console/node_modules/.bin/tsc +21 -0
  52. package/packages/console/node_modules/.bin/tsserver +21 -0
  53. package/packages/console/package.json +26 -0
  54. package/packages/console/src/Commands/MakeController.ts +1 -0
  55. package/packages/console/src/Commands/MakeModel.ts +1 -0
  56. package/packages/console/src/Commands/MakeResource.ts +1 -0
  57. package/packages/console/src/Contracts/.gitkeep +0 -0
  58. package/packages/console/src/IO/.gitkeep +0 -0
  59. package/packages/console/src/Kernel.ts +1 -0
  60. package/packages/console/src/Musketeer.ts +4 -0
  61. package/packages/console/src/Providers/ConsoleServiceProvider.ts +16 -0
  62. package/packages/console/src/index.ts +10 -0
  63. package/packages/console/tests/.gitkeep +0 -0
  64. package/packages/console/tsconfig.json +8 -0
  65. package/packages/console/vite.config.ts +9 -0
  66. package/packages/core/node_modules/.bin/tsc +21 -0
  67. package/packages/core/node_modules/.bin/tsserver +21 -0
  68. package/packages/core/package.json +29 -0
  69. package/packages/core/src/Application.ts +164 -0
  70. package/packages/core/src/Container.ts +71 -0
  71. package/packages/core/src/Contracts/.gitkeep +0 -0
  72. package/packages/core/src/Contracts/BindingsContract.ts +37 -0
  73. package/packages/core/src/Controller.ts +20 -0
  74. package/packages/core/src/Exceptions/Handler.ts +1 -0
  75. package/packages/core/src/Http/Kernel.ts +47 -0
  76. package/packages/core/src/Providers/AppServiceProvider.ts +18 -0
  77. package/packages/core/src/Providers/ViewServiceProvider.ts +19 -0
  78. package/packages/core/src/ServiceProvider.ts +21 -0
  79. package/packages/core/src/Utils/PathLoader.ts +46 -0
  80. package/packages/core/src/index.ts +14 -0
  81. package/packages/core/tests/.gitkeep +0 -0
  82. package/packages/core/tsconfig.json +8 -0
  83. package/packages/core/vite.config.ts +9 -0
  84. package/packages/database/node_modules/.bin/tsc +21 -0
  85. package/packages/database/node_modules/.bin/tsserver +21 -0
  86. package/packages/database/package.json +26 -0
  87. package/packages/database/src/Contracts/.gitkeep +0 -0
  88. package/packages/database/src/Migrations/.gitkeep +0 -0
  89. package/packages/database/src/Model.ts +1 -0
  90. package/packages/database/src/Providers/DatabaseServiceProvider.ts +16 -0
  91. package/packages/database/src/Query/.gitkeep +0 -0
  92. package/packages/database/src/index.ts +6 -0
  93. package/packages/database/src/tests/.gitkeep +0 -0
  94. package/packages/database/tsconfig.json +8 -0
  95. package/packages/database/vite.config.ts +9 -0
  96. package/packages/http/node_modules/.bin/tsc +21 -0
  97. package/packages/http/node_modules/.bin/tsserver +21 -0
  98. package/packages/http/package.json +28 -0
  99. package/packages/http/src/Contracts/ControllerContracts.ts +13 -0
  100. package/packages/http/src/Contracts/HttpContract.ts +7 -0
  101. package/packages/http/src/Middleware/LogRequests.ts +9 -0
  102. package/packages/http/src/Middleware.ts +5 -0
  103. package/packages/http/src/Providers/HttpServiceProvider.ts +22 -0
  104. package/packages/http/src/Request.ts +59 -0
  105. package/packages/http/src/Resources/ApiResource.ts +39 -0
  106. package/packages/http/src/Resources/JsonResource.ts +201 -0
  107. package/packages/http/src/Response.ts +80 -0
  108. package/packages/http/src/index.ts +13 -0
  109. package/packages/http/tests/.gitkeep +0 -0
  110. package/packages/http/tsconfig.json +8 -0
  111. package/packages/http/vite.config.ts +9 -0
  112. package/packages/mail/node_modules/.bin/tsc +21 -0
  113. package/packages/mail/node_modules/.bin/tsserver +21 -0
  114. package/packages/mail/package.json +26 -0
  115. package/packages/mail/src/Contracts/.gitkeep +0 -0
  116. package/packages/mail/src/Drivers/SES.ts +1 -0
  117. package/packages/mail/src/Drivers/SMTP.ts +1 -0
  118. package/packages/mail/src/Helpers.ts +1 -0
  119. package/packages/mail/src/Mailable.ts +1 -0
  120. package/packages/mail/src/Mailer.ts +1 -0
  121. package/packages/mail/src/Providers/MailServiceProvider.ts +16 -0
  122. package/packages/mail/src/index.ts +10 -0
  123. package/packages/mail/tests/.gitkeep +0 -0
  124. package/packages/mail/tsconfig.json +8 -0
  125. package/packages/mail/vite.config.ts +9 -0
  126. package/packages/queue/node_modules/.bin/tsc +21 -0
  127. package/packages/queue/node_modules/.bin/tsserver +21 -0
  128. package/packages/queue/package.json +26 -0
  129. package/packages/queue/src/Contracts/.gitkeep +0 -0
  130. package/packages/queue/src/Drivers/MemoryDriver.ts +1 -0
  131. package/packages/queue/src/Drivers/RedisDriver.ts +1 -0
  132. package/packages/queue/src/Jobs/.gitkeep +0 -0
  133. package/packages/queue/src/Providers/QueueServiceProvider.ts +16 -0
  134. package/packages/queue/src/QueueManager.ts +1 -0
  135. package/packages/queue/src/Workers/.gitkeep +0 -0
  136. package/packages/queue/src/index.ts +8 -0
  137. package/packages/queue/tests/.gitkeep +0 -0
  138. package/packages/queue/tsconfig.json +8 -0
  139. package/packages/queue/vite.config.ts +9 -0
  140. package/packages/router/node_modules/.bin/tsc +21 -0
  141. package/packages/router/node_modules/.bin/tsserver +21 -0
  142. package/packages/router/package.json +27 -0
  143. package/packages/router/src/Contracts/.gitkeep +0 -0
  144. package/packages/router/src/Controller.ts +1 -0
  145. package/packages/router/src/Decorators/ApiResource.ts +1 -0
  146. package/packages/router/src/Decorators/Controller.ts +1 -0
  147. package/packages/router/src/Decorators/Get.ts +1 -0
  148. package/packages/router/src/Decorators/Middleware.ts +1 -0
  149. package/packages/router/src/Decorators/Post.ts +1 -0
  150. package/packages/router/src/Middleware/.gitkeep +0 -0
  151. package/packages/router/src/Providers/AssetsServiceProvider.ts +53 -0
  152. package/packages/router/src/Providers/RouteServiceProvider.ts +44 -0
  153. package/packages/router/src/Route.ts +1 -0
  154. package/packages/router/src/Router.ts +180 -0
  155. package/packages/router/src/index.ts +14 -0
  156. package/packages/router/tests/.gitkeep +0 -0
  157. package/packages/router/tsconfig.json +8 -0
  158. package/packages/router/vite.config.ts +9 -0
  159. package/packages/support/node_modules/.bin/tsc +21 -0
  160. package/packages/support/node_modules/.bin/tsserver +21 -0
  161. package/packages/support/package.json +26 -0
  162. package/packages/support/src/Contracts/ObjContract.ts +53 -0
  163. package/packages/support/src/Contracts/StrContract.ts +6 -0
  164. package/packages/support/src/Facades/.gitkeep +0 -0
  165. package/packages/support/src/Helpers/Arr.ts +33 -0
  166. package/packages/support/src/Helpers/Number.ts +194 -0
  167. package/packages/support/src/Helpers/Obj.ts +220 -0
  168. package/packages/support/src/Helpers/Str.ts +256 -0
  169. package/packages/support/src/index.ts +10 -0
  170. package/packages/support/tests/.gitkeep +0 -0
  171. package/packages/support/tsconfig.json +8 -0
  172. package/packages/support/vite.config.ts +9 -0
  173. package/pnpm-workspace.yaml +7 -0
  174. package/tsconfig.json +26 -0
  175. package/tsup.config.ts +15 -0
  176. package/vite.base.config.ts +12 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Abbreviates large numbers using SI symbols (K, M, B...)
3
+ * and formats the output according to the given locale.
4
+ *
5
+ * @param value - The number to abbreviate
6
+ * @param locale - Optional locale string (default: "en-US")
7
+ * @returns A localized, abbreviated number string
8
+ */
9
+ export const abbreviate = (value?: number, locale: string = 'en-US'): string => {
10
+ if (!value) return '0'
11
+
12
+ // Numbers less than 1000 don't need abbreviation
13
+ if (value < 1000) {
14
+ return new Intl.NumberFormat(locale).format(value)
15
+ }
16
+
17
+ const si = [
18
+ { v: 1e18, s: 'E' },
19
+ { v: 1e15, s: 'P' },
20
+ { v: 1e12, s: 'T' },
21
+ { v: 1e9, s: 'B' },
22
+ { v: 1e6, s: 'M' },
23
+ { v: 1e3, s: 'K' },
24
+ ]
25
+
26
+ const match = si.find(scale => value >= scale.v)
27
+ if (!match) return new Intl.NumberFormat(locale).format(value)
28
+
29
+ const formatted = value / match.v
30
+
31
+ return (
32
+ new Intl.NumberFormat(locale, {
33
+ minimumFractionDigits: 0,
34
+ maximumFractionDigits: 2,
35
+ }).format(formatted) + match.s
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Concverts a number into human readable string
41
+ *
42
+ * @param num The number to convert
43
+ * @param slugify convert the ouput into a slug using this as a separator
44
+ * @returns
45
+ */
46
+ export const humanize = (num: number, slugify?: '-' | '_'): string => {
47
+ if (!num) {
48
+ return ''
49
+ }
50
+
51
+ if (slugify === '-' || slugify === '_') {
52
+ const h = humanize(num)
53
+ return typeof h === 'string' ? h.replace(' ', slugify).toLowerCase() : h
54
+ }
55
+
56
+ const ones = [
57
+ '',
58
+ 'one',
59
+ 'two',
60
+ 'three',
61
+ 'four',
62
+ 'five',
63
+ 'six',
64
+ 'seven',
65
+ 'eight',
66
+ 'nine',
67
+ 'ten',
68
+ 'eleven',
69
+ 'twelve',
70
+ 'thirteen',
71
+ 'fourteen',
72
+ 'fifteen',
73
+ 'sixteen',
74
+ 'seventeen',
75
+ 'eighteen',
76
+ 'nineteen',
77
+ ]
78
+ const tens = [
79
+ '',
80
+ '',
81
+ 'twenty',
82
+ 'thirty',
83
+ 'forty',
84
+ 'fifty',
85
+ 'sixty',
86
+ 'seventy',
87
+ 'eighty',
88
+ 'ninety',
89
+ ]
90
+
91
+ const numString: string = num.toString()
92
+
93
+ if (num < 0) throw new Error('Negative numbers are not supported.')
94
+
95
+ if (num === 0) return 'zero'
96
+
97
+ //the case of 1 - 20
98
+ if (num < 20) {
99
+ return ones[num] ?? ''
100
+ }
101
+
102
+ if (numString.length === 2) {
103
+ return tens[numString[0] as unknown as number] + ' ' + ones[numString[1] as unknown as number]
104
+ }
105
+
106
+ //100 and more
107
+ if (numString.length == 3) {
108
+ if (numString[1] === '0' && numString[2] === '0')
109
+ return ones[numString[0] as unknown as number] + ' hundred'
110
+ else
111
+ return (
112
+ ones[numString[0] as unknown as number] +
113
+ ' hundred and ' +
114
+ humanize(+((numString[1] || '') + numString[2]), slugify)
115
+ )
116
+ }
117
+
118
+ if (numString.length === 4) {
119
+ const end = +((numString[1] || '') + numString[2] + numString[3])
120
+ if (end === 0) return ones[numString[0] as unknown as number] + ' thousand'
121
+ if (end < 100)
122
+ return ones[numString[0] as unknown as number] + ' thousand and ' + humanize(end, slugify)
123
+ return ones[numString[0] as unknown as number] + ' thousand ' + humanize(end, slugify)
124
+ }
125
+
126
+ return num as unknown as string
127
+ }
128
+
129
+ /**
130
+ * Converts a number of bytes into a human-readable string.
131
+ *
132
+ * @param bytes - The size in bytes to convert
133
+ * @param decimals - Number of decimal places to display (default: 2)
134
+ * @param bits - If true, uses 1000-based (SI) units (B, KB, MB...);
135
+ * otherwise uses 1024-based binary units (Bytes, KiB...)
136
+ * @returns A formatted string with the appropriate unit
137
+ */
138
+ export const toBytes = (
139
+ bytes?: number,
140
+ decimals: number = 2,
141
+ bits: boolean = false
142
+ ): string => {
143
+ if (!bytes || isNaN(bytes)) {
144
+ return bits ? '0 B' : '0 Bytes'
145
+ }
146
+
147
+ const base = bits ? 1000 : 1024
148
+ const dm = decimals < 0 ? 0 : decimals
149
+ const sizes = bits
150
+ ? ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] // SI units
151
+ : ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] // Binary units
152
+
153
+ const index = Math.floor(Math.log(bytes) / Math.log(base))
154
+
155
+ const value = parseFloat((bytes / Math.pow(base, index)).toFixed(dm))
156
+ return `${value} ${sizes[index]}`
157
+ }
158
+
159
+ /**
160
+ * Formats a duration (in seconds) into a human-readable string.
161
+ *
162
+ * @param seconds - Duration in seconds
163
+ * @param worded - If true, outputs worded format (e.g., "1hr 2min 3sec"),
164
+ * otherwise HH:MM:SS (e.g., "01:02:03")
165
+ * @returns A formatted time string
166
+ */
167
+ export const toHumanTime = (
168
+ seconds: number = 0,
169
+ worded: boolean = false
170
+ ): string => {
171
+ // Ensure seconds is a number and not negative
172
+ if (isNaN(seconds) || seconds < 0) seconds = 0
173
+
174
+ const hours = Math.floor(seconds / 3600)
175
+ const minutes = Math.floor((seconds % 3600) / 60)
176
+ const secs = Math.floor(seconds % 60)
177
+
178
+ // Worded format → "1hr 2min 3sec"
179
+ if (worded) {
180
+ const parts = []
181
+ if (hours) parts.push(`${hours}hr`)
182
+ if (minutes) parts.push(`${minutes}min`)
183
+ if (secs || (!hours && !minutes)) parts.push(`${secs}sec`)
184
+ return parts.join(' ')
185
+ }
186
+
187
+ // HH:MM:SS format → zero-padded
188
+ const hh = hours > 0 ? `${hours}:` : ''
189
+ const mm = (hours > 0 && minutes < 10 ? `0${minutes}` : minutes) + ':'
190
+ const ss = secs < 10 ? `0${secs}` : secs
191
+
192
+ return `${hh}${mm}${ss}`
193
+ }
194
+
@@ -0,0 +1,220 @@
1
+ import { DotFlatten, DotNestedKeys, DotNestedValue, KeysToSnakeCase } from "../Contracts/ObjContract"
2
+
3
+ /**
4
+ * Flattens a nested object into a single-level object
5
+ * with dot-separated keys.
6
+ *
7
+ * Example:
8
+ * doter({
9
+ * user: { name: "John", address: { city: "NY" } },
10
+ * active: true
11
+ * })
12
+ *
13
+ * Output:
14
+ * {
15
+ * "user.name": "John",
16
+ * "user.address.city": "NY",
17
+ * "active": true
18
+ * }
19
+ *
20
+ * @template T - The type of the input object
21
+ * @param obj - The nested object to flatten
22
+ * @returns A flattened object with dotted keys and inferred types
23
+ */
24
+ export const dot = <T extends Record<string, any>> (obj: T): DotFlatten<T> => {
25
+ const result = {} as Record<string, unknown>
26
+
27
+ /**
28
+ * Internal recursive function to traverse and flatten the object.
29
+ *
30
+ * @param o - Current object to flatten
31
+ * @param prefix - Key path accumulated so far
32
+ */
33
+ const recurse = (o: Record<string, any>, prefix = ''): void => {
34
+ for (const [key, value] of Object.entries(o)) {
35
+ const newKey = prefix ? `${prefix}.${key}` : key
36
+
37
+ /**
38
+ * Recurse if the value is a plain object
39
+ */
40
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
41
+ recurse(value, newKey)
42
+ } else {
43
+ /**
44
+ * Otherwise, assign directly
45
+ */
46
+ result[newKey] = value
47
+ }
48
+ }
49
+ }
50
+
51
+ recurse(obj)
52
+ return result as DotFlatten<T>
53
+ }
54
+
55
+ /**
56
+ * Extracts a subset of properties from an object.
57
+ *
58
+ * @template T - Type of the source object
59
+ * @template K - Keys of T to extract
60
+ * @param obj - The source object
61
+ * @param keys - Array of keys to extract
62
+ * @returns A new object with only the specified keys
63
+ */
64
+ export const extractProperties = <T extends object, K extends keyof T> (
65
+ obj: T,
66
+ keys: readonly K[] = []
67
+ ): Pick<T, K> => {
68
+ return Object.fromEntries(
69
+ keys.map(key => [key, obj[key]])
70
+ ) as Pick<T, K>
71
+ }
72
+
73
+ /**
74
+ * Safely retrieves a value from an object by key or nested keys.
75
+ *
76
+ * @template T - Type of the source object
77
+ * @param key - Single key or tuple [parentKey, childKey]
78
+ * @param item - The source object
79
+ * @returns The found value as a string or the key itself if not found
80
+ */
81
+ export const getValue = <
82
+ T extends Record<string, any> // Allow nested objects
83
+ > (
84
+ key: string | [keyof T, keyof T[string]],
85
+ item: T
86
+ ): string => {
87
+ if (Array.isArray(key)) {
88
+ const [parent, child] = key
89
+
90
+ if (child !== undefined) {
91
+ // Access nested property: item[parent][child]
92
+ return (
93
+ String(item?.[parent]?.[child] ??
94
+ item?.[parent] ??
95
+ `${String(parent)}.${String(child)}`)
96
+ )
97
+ }
98
+
99
+ // Only top-level key
100
+ return String(item?.[parent] ?? parent)
101
+ }
102
+
103
+ // Single key access
104
+ return String(item?.[key] ?? key)
105
+ }
106
+
107
+ /**
108
+ * Maps over an object's entries and returns a new object
109
+ * with transformed keys and/or values.
110
+ *
111
+ * @template T - Type of the input object
112
+ * @template R - Type of the new values
113
+ * @param obj - The object to transform
114
+ * @param callback - Function that receives [key, value] and returns [newKey, newValue]
115
+ * @returns A new object with transformed entries
116
+ */
117
+ export const modObj = <T extends object, R> (
118
+ obj: T,
119
+ callback: (entry: [keyof T & string, T[keyof T]]) => [string, R]
120
+ ): Record<string, R> => {
121
+ return Object.fromEntries(
122
+ Object.entries(obj).map(([key, value]) =>
123
+ callback([key as keyof T & string, value as T[keyof T]])
124
+ )
125
+ ) as Record<string, R>
126
+ }
127
+
128
+
129
+ export function safeDot<T extends Record<string, any>> (data: T): T
130
+ export function safeDot<
131
+ T extends Record<string, any>,
132
+ K extends DotNestedKeys<T>
133
+ > (data: T, key?: K): DotNestedValue<T, K>
134
+ export function safeDot<
135
+ T extends Record<string, any>,
136
+ K extends DotNestedKeys<T>
137
+ > (data: T, key?: K): any {
138
+ if (!key) return data
139
+ return key.split('.').reduce((acc: any, k) => acc?.[k], data)
140
+ }
141
+
142
+ /**
143
+ * Sets a nested property on an object using dot notation.
144
+ *
145
+ * @example
146
+ * const obj = {}
147
+ * setNested(obj, 'app.user.name', 'Legacy')
148
+ * console.log(obj)
149
+ * // Output: { app: { user: { name: 'Legacy' } } }
150
+ *
151
+ * @param obj - The target object to modify.
152
+ * @param key - The dot-separated key (e.g., 'app.user.name').
153
+ * @param value - The value to set at the specified path.
154
+ */
155
+ export const setNested = (
156
+ obj: Record<string, any>,
157
+ key: string,
158
+ value: any
159
+ ): void => {
160
+ if (!key.includes('.')) {
161
+ obj[key] = value
162
+ return
163
+ }
164
+
165
+ const parts = key.split('.')
166
+ let current = obj
167
+
168
+ for (let i = 0; i < parts.length; i++) {
169
+ const part = parts[i]
170
+
171
+ /**
172
+ * If we're at the last key, assign the value
173
+ */
174
+ if (i === parts.length - 1) {
175
+ current[part] = value
176
+ } else {
177
+ /**
178
+ * If the key doesn't exist or isn't an object, create it
179
+ */
180
+ if (typeof current[part] !== 'object' || current[part] === null) {
181
+ current[part] = {}
182
+ }
183
+ current = current[part]
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Converts object keys to a slugified format (e.g., snake_case).
190
+ *
191
+ * @template T - Type of the input object
192
+ * @param obj - The object whose keys will be slugified
193
+ * @param only - Optional array of keys to slugify (others remain unchanged)
194
+ * @param separator - Separator for slugified keys (default: "_")
195
+ * @returns A new object with slugified keys
196
+ */
197
+ export const slugifyKeys = <T extends object> (
198
+ obj: T,
199
+ only: string[] = [],
200
+ separator: string = '_'
201
+ ): KeysToSnakeCase<T> => {
202
+ const slugify = (key: string): string =>
203
+ key
204
+ .replace(/([a-z])([A-Z])/g, `$1${separator}$2`) // Handle camelCase
205
+ .replace(/[\s\W]+/g, separator) // Replace spaces/symbols
206
+ .replace(new RegExp(`${separator}{2,}`, 'g'), separator) // Remove duplicate separators
207
+ .replace(new RegExp(`^${separator}|${separator}$`, 'g'), '') // Trim edges
208
+ .toLowerCase()
209
+
210
+ let entries = Object.entries(obj)
211
+
212
+ // Filter if `only` is provided
213
+ if (only.length) {
214
+ entries = entries.filter(([key]) => only.includes(key))
215
+ }
216
+
217
+ return Object.fromEntries(
218
+ entries.map(([key, value]) => [slugify(key), value])
219
+ ) as KeysToSnakeCase<T>
220
+ }
@@ -0,0 +1,256 @@
1
+ import { dot } from "./Obj"
2
+
3
+ /**
4
+ * Get the portion of the string after the first occurrence of the given value.
5
+ *
6
+ * @param value
7
+ * @param search
8
+ * @returns
9
+ */
10
+ export const after = (value: string, search: string): string => {
11
+ if (!search) return value
12
+ const index = value.indexOf(search)
13
+ return index !== -1 ? value.slice(index + search.length) : value
14
+ }
15
+
16
+ /**
17
+ * Get the portion of the string after the last occurrence of the given value.
18
+ *
19
+ * @param value
20
+ * @param search
21
+ * @returns
22
+ */
23
+ export const afterLast = (value: string, search: string): string => {
24
+ if (!search) return value
25
+ const lastIndex = value.lastIndexOf(search)
26
+ return lastIndex !== -1 ? value.slice(lastIndex + search.length) : value
27
+ }
28
+
29
+ /**
30
+ * Get the portion of the string before the first occurrence of the given value.
31
+ *
32
+ * @param value
33
+ * @param search
34
+ * @returns
35
+ */
36
+ export const before = (value: string, search: string): string => {
37
+ if (!search) return value
38
+ const index = value.indexOf(search)
39
+ return index !== -1 ? value.slice(0, index) : value
40
+ }
41
+
42
+ /**
43
+ * Get the portion of the string before the last occurrence of the given value.
44
+ *
45
+ * @param value
46
+ * @param search
47
+ * @returns
48
+ */
49
+ export const beforeLast = (value: string, search: string): string => {
50
+ if (!search) return value
51
+ const lastIndex = value.lastIndexOf(search)
52
+ return lastIndex !== -1 ? value.slice(0, lastIndex) : value
53
+ }
54
+
55
+ /**
56
+ * Capitalizes the first character of a string.
57
+ *
58
+ * @param str - The input string
59
+ * @returns The string with the first character capitalized
60
+ */
61
+ export function capitalize (str: string): string {
62
+ if (!str) return '' // Handle empty or undefined strings safely
63
+ return str[0].toUpperCase() + str.slice(1)
64
+ }
65
+
66
+
67
+ /**
68
+ * Returns the pluralized form of a word based on the given number.
69
+ *
70
+ * @param word - The word to pluralize
71
+ * @param count - The number determining pluralization
72
+ * @returns Singular if count === 1, otherwise plural form
73
+ */
74
+ export const pluralize = (word: string, count: number): string => {
75
+ // If count is exactly 1 → singular
76
+ if (count === 1) return word
77
+
78
+ // Irregular plurals map
79
+ const irregularPlurals: Record<string, string> = {
80
+ foot: 'feet',
81
+ child: 'children',
82
+ mouse: 'mice',
83
+ goose: 'geese',
84
+ person: 'people',
85
+ man: 'men',
86
+ woman: 'women',
87
+ }
88
+
89
+ // Handle irregular cases first
90
+ if (word in irregularPlurals) {
91
+ return irregularPlurals[word]
92
+ }
93
+
94
+ // If word ends with consonant + "y" → replace "y" with "ies"
95
+ if (
96
+ word.endsWith('y') &&
97
+ !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2]?.toLowerCase() ?? '')
98
+ ) {
99
+ return word.slice(0, -1) + 'ies'
100
+ }
101
+
102
+ // If word ends in "s", "ss", "sh", "ch", "x", or "z" → add "es"
103
+ if (/(s|ss|sh|ch|x|z)$/i.test(word)) {
104
+ return word + 'es'
105
+ }
106
+
107
+ // Default: just add "s"
108
+ return word + 's'
109
+ }
110
+
111
+ /**
112
+ * Converts a plural English word into its singular form.
113
+ *
114
+ * @param word - The word to singularize
115
+ * @returns The singular form of the word
116
+ */
117
+ export const singularize = (word: string): string => {
118
+ // Irregular plurals map (reverse of pluralize)
119
+ const irregulars: Record<string, string> = {
120
+ feet: 'foot',
121
+ children: 'child',
122
+ mice: 'mouse',
123
+ geese: 'goose',
124
+ people: 'person',
125
+ men: 'man',
126
+ women: 'woman',
127
+ }
128
+
129
+ // Handle irregular cases
130
+ if (word in irregulars) return irregulars[word]
131
+
132
+ // Words ending in "ies" → change to "y" (e.g., "bodies" → "body")
133
+ if (/ies$/i.test(word) && word.length > 3) {
134
+ return word.replace(/ies$/i, 'y')
135
+ }
136
+
137
+ // Words ending in "es" after certain consonants → remove "es"
138
+ if (/(ches|shes|sses|xes|zes)$/i.test(word)) {
139
+ return word.replace(/es$/i, '')
140
+ }
141
+
142
+ // Generic case: remove trailing "s"
143
+ if (/s$/i.test(word) && word.length > 1) {
144
+ return word.replace(/s$/i, '')
145
+ }
146
+
147
+ return word
148
+ }
149
+
150
+ /**
151
+ * Converts a string into a slug format.
152
+ * Handles camelCase, spaces, and non-alphanumeric characters.
153
+ *
154
+ * @param str - The input string to slugify
155
+ * @param joiner - The character used to join words (default: "_")
156
+ * @returns A slugified string
157
+ */
158
+ export const slugify = (str: string, joiner = '_'): string => {
159
+ return str
160
+ // Handle camelCase by adding joiner between lowercase → uppercase
161
+ .replace(/([a-z])([A-Z])/g, `$1${joiner}$2`)
162
+ // Replace spaces and non-alphanumeric characters with joiner
163
+ .replace(/[\s\W]+/g, joiner)
164
+ // Remove duplicate joiners
165
+ .replace(new RegExp(`${joiner}{2,}`, 'g'), joiner)
166
+ // Trim joiners from start/end
167
+ .replace(new RegExp(`^${joiner}|${joiner}$`, 'g'), '')
168
+ .toLowerCase()
169
+ }
170
+
171
+ /**
172
+ * Truncates a string to a specified length and appends an ellipsis if needed.
173
+ *
174
+ * @param str - The input string
175
+ * @param len - Maximum length of the result (including ellipsis)
176
+ * @param ellipsis - String to append if truncated (default: "...")
177
+ * @returns The truncated string
178
+ */
179
+ export const subString = (
180
+ str: string,
181
+ len: number,
182
+ ellipsis: string = '...'
183
+ ): string => {
184
+ if (!str) return ''
185
+ if (len <= ellipsis.length) return ellipsis // Avoid negative slicing
186
+
187
+ return str.length > len
188
+ ? str.substring(0, len - ellipsis.length).trimEnd() + ellipsis
189
+ : str
190
+ }
191
+
192
+ /**
193
+ * Replaces placeholders in a string with corresponding values from a data object.
194
+ *
195
+ * Example:
196
+ * substitute("Hello { user.name }!", { user: { name: "John" } })
197
+ * // "Hello John!"
198
+ *
199
+ * @param str - The string containing placeholders wrapped in { } braces.
200
+ * @param data - Object containing values to substitute. Supports nested keys via dot notation.
201
+ * @param def - Default value to use if a key is missing. (Optional)
202
+ * @returns The substituted string or undefined if the input string or data is invalid.
203
+ */
204
+ export const substitute = (
205
+ str: string,
206
+ data: Record<string, unknown> = {},
207
+ def?: string
208
+ ): string | undefined => {
209
+ if (!str || !data) return undefined
210
+
211
+ // Matches { key } or { nested.key } placeholders
212
+ const regex = /{\s*([a-zA-Z0-9_.]+)\s*}/g
213
+
214
+ // Flatten the data so we can directly access dot notation keys
215
+ const flattened = dot(data)
216
+
217
+ // Replace each placeholder with its value or the default
218
+ const out = str.replace(regex, (_, key: string) => {
219
+ const value = flattened[key]
220
+ return value !== undefined ? String(value) : def ?? ''
221
+ })
222
+
223
+ return out
224
+ }
225
+
226
+ /**
227
+ * Truncates a string to a specified length, removing HTML tags and
228
+ * appending a suffix if the string exceeds the length.
229
+ *
230
+ * @param str - The string to truncate
231
+ * @param len - Maximum length (default: 20)
232
+ * @param suffix - Suffix to append if truncated (default: "...")
233
+ * @returns The truncated string
234
+ */
235
+ export const truncate = (
236
+ str: string,
237
+ len: number = 20,
238
+ suffix: string = '...'
239
+ ): string => {
240
+ if (!str) return ''
241
+
242
+ // Remove any HTML tags
243
+ const clean = str.replace(/<[^>]+>/g, '')
244
+
245
+ // Determine if we need to truncate
246
+ const truncated =
247
+ clean.length > len
248
+ ? clean.substring(0, len - suffix.length) + suffix
249
+ : clean
250
+
251
+ // Normalize spaces and line breaks
252
+ return truncated
253
+ .replace(/\n/g, ' ') // Replace all line breaks
254
+ .replace(new RegExp(`\\s+${suffix.replace(/\./g, '\\.')}$`), suffix) // Avoid extra space before suffix
255
+ }
256
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file Automatically generated by barrelsby.
3
+ */
4
+
5
+ export * from './Contracts/ObjContract';
6
+ export * from './Contracts/StrContract';
7
+ export * from './Helpers/Arr';
8
+ export * from './Helpers/Number';
9
+ export * from './Helpers/Obj';
10
+ export * from './Helpers/Str';
File without changes
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src"],
7
+ "exclude": ["dist", "node_modules"]
8
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm', 'cjs'],
6
+ dts: true,
7
+ sourcemap: true,
8
+ clean: true
9
+ })