@stacksjs/bun-router 0.0.17 → 0.0.18
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/cli.js +1 -1
- package/dist/container/service-provider.d.ts +3 -3
- package/dist/development/performance-profiler.d.ts +3 -3
- package/dist/development/route-debugger.d.ts +1 -1
- package/dist/development/route-inspector.d.ts +2 -2
- package/dist/observability/integration.d.ts +1 -1
- package/dist/response/macros.d.ts +1 -1
- package/dist/router/index.d.ts +7 -7
- package/dist/router/router.d.ts +0 -32
- package/dist/testing/performance-testing.d.ts +2 -2
- package/dist/testing/test-client.d.ts +1 -1
- package/dist/testing/websocket-testing.d.ts +1 -1
- package/package.json +2 -3
- package/src/auth.ts +0 -489
- package/src/cache/lru-cache.ts +0 -457
- package/src/cache/middleware-memoization.ts +0 -531
- package/src/cache/route-cache-warmer.ts +0 -486
- package/src/cache/sqlite-cache.ts +0 -783
- package/src/cache/streaming-cache.ts +0 -572
- package/src/cli/colors.ts +0 -29
- package/src/cli/index.ts +0 -287
- package/src/cli/middleware.ts +0 -291
- package/src/cli/openapi.ts +0 -407
- package/src/cli/router.ts +0 -188
- package/src/cli/routes.ts +0 -265
- package/src/cli/utils.ts +0 -531
- package/src/cli.ts +0 -5
- package/src/config.ts +0 -322
- package/src/container/container.ts +0 -786
- package/src/container/contextual-binding.ts +0 -606
- package/src/container/decorators.ts +0 -368
- package/src/container/index.ts +0 -49
- package/src/container/service-provider.ts +0 -596
- package/src/development/hot-reload.ts +0 -673
- package/src/development/index.ts +0 -499
- package/src/development/performance-profiler.ts +0 -717
- package/src/development/route-debugger.ts +0 -527
- package/src/development/route-inspector.ts +0 -749
- package/src/development/typescript-utilities.ts +0 -682
- package/src/docs.ts +0 -397
- package/src/errors/circuit-breaker.ts +0 -733
- package/src/errors/error-handler.ts +0 -569
- package/src/errors/error-reporting.ts +0 -672
- package/src/errors/exceptions.ts +0 -536
- package/src/errors/graceful-degradation.ts +0 -623
- package/src/errors/index.ts +0 -21
- package/src/errors/router-errors.ts +0 -632
- package/src/file-serving/static-files.ts +0 -718
- package/src/index.ts +0 -66
- package/src/middleware/auth.ts +0 -220
- package/src/middleware/content_security_policy.ts +0 -215
- package/src/middleware/cors.ts +0 -142
- package/src/middleware/csrf.ts +0 -166
- package/src/middleware/ddos_protection.ts +0 -279
- package/src/middleware/file_security.ts +0 -191
- package/src/middleware/file_upload.ts +0 -275
- package/src/middleware/helmet.ts +0 -268
- package/src/middleware/index.ts +0 -117
- package/src/middleware/input_validation.ts +0 -453
- package/src/middleware/json_body.ts +0 -37
- package/src/middleware/performance_alerting.ts +0 -540
- package/src/middleware/performance_dashboard.ts +0 -661
- package/src/middleware/performance_monitor.ts +0 -945
- package/src/middleware/pipeline.ts +0 -489
- package/src/middleware/rate_limit.ts +0 -251
- package/src/middleware/request_id.ts +0 -36
- package/src/middleware/request_signing.ts +0 -638
- package/src/middleware/request_tracer.ts +0 -653
- package/src/middleware/response_cache.ts +0 -778
- package/src/middleware/security.ts +0 -482
- package/src/middleware/security_suite.ts +0 -257
- package/src/middleware/session.ts +0 -91
- package/src/model-binding/index.ts +0 -18
- package/src/model-binding/model-middleware.ts +0 -425
- package/src/model-binding/model-registry.ts +0 -550
- package/src/model-binding.ts +0 -370
- package/src/model-resolver-factory.ts +0 -106
- package/src/observability/correlation.ts +0 -691
- package/src/observability/health-checks.ts +0 -729
- package/src/observability/index.ts +0 -184
- package/src/observability/integration.ts +0 -548
- package/src/observability/metrics.ts +0 -753
- package/src/observability/tracing.ts +0 -638
- package/src/optimization/bun-utilities.ts +0 -778
- package/src/query-builder-integration.ts +0 -137
- package/src/request/context.ts +0 -109
- package/src/request/enhanced-request.ts +0 -876
- package/src/request/macros.ts +0 -822
- package/src/response/macros.ts +0 -665
- package/src/response/response-factory.ts +0 -596
- package/src/router/api-routes.ts +0 -243
- package/src/router/file-based-routing.ts +0 -697
- package/src/router/file-streaming.ts +0 -383
- package/src/router/fluent-routing.ts +0 -866
- package/src/router/group-organization.ts +0 -229
- package/src/router/handler-resolver.ts +0 -327
- package/src/router/http-methods.ts +0 -407
- package/src/router/index.ts +0 -193
- package/src/router/middleware-groups.ts +0 -222
- package/src/router/middleware-integration.ts +0 -399
- package/src/router/middleware.ts +0 -234
- package/src/router/model-binding.ts +0 -215
- package/src/router/optimized-route-matching.ts +0 -302
- package/src/router/route-building.ts +0 -221
- package/src/router/route-compiler.ts +0 -691
- package/src/router/route-matching.ts +0 -368
- package/src/router/route-trie.ts +0 -450
- package/src/router/router.ts +0 -1616
- package/src/router/server.ts +0 -684
- package/src/router/validation-integration.ts +0 -449
- package/src/router/view-rendering.ts +0 -233
- package/src/router/websocket.ts +0 -100
- package/src/routing/route-caching.ts +0 -402
- package/src/routing/route-throttling.ts +0 -469
- package/src/routing/subdomain-routing.ts +0 -492
- package/src/session/database-store.ts +0 -109
- package/src/session/file-store.ts +0 -148
- package/src/session/index.ts +0 -244
- package/src/session/memory-store.ts +0 -88
- package/src/session/redis-store.ts +0 -93
- package/src/streaming/index.ts +0 -17
- package/src/streaming/sse-handler.ts +0 -482
- package/src/streaming/stream-handler.ts +0 -552
- package/src/testing/auth-testing.ts +0 -448
- package/src/testing/file-upload-testing.ts +0 -543
- package/src/testing/index.ts +0 -10
- package/src/testing/middleware-testing.ts +0 -322
- package/src/testing/model-binding-testing.ts +0 -645
- package/src/testing/performance-testing.ts +0 -738
- package/src/testing/test-client.ts +0 -310
- package/src/testing/test-request.ts +0 -315
- package/src/testing/test-response.ts +0 -332
- package/src/testing/types.ts +0 -224
- package/src/testing/websocket-testing.ts +0 -590
- package/src/types/controller-types.ts +0 -385
- package/src/types/core.ts +0 -683
- package/src/types/middleware-types.ts +0 -422
- package/src/types/request-response-augmentation.ts +0 -489
- package/src/types/route-inference.ts +0 -372
- package/src/types.ts +0 -2123
- package/src/url.ts +0 -131
- package/src/utils/index.ts +0 -1
- package/src/utils/query-preservation.ts +0 -201
- package/src/utils.ts +0 -395
- package/src/validation/validator.ts +0 -688
- package/src/websocket/clustering.ts +0 -769
package/src/utils.ts
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import type { ActionHandler, ActionHandlerClass, RouteHandler } from './types'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
// Re-export query preservation utilities
|
|
5
|
-
export * from './utils/query-preservation'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Normalizes a path by ensuring it starts with a forward slash and has no trailing slash
|
|
9
|
-
* @param path The path to normalize
|
|
10
|
-
*/
|
|
11
|
-
export function normalizePath(path: string): string {
|
|
12
|
-
// First replace double (or more) slashes with a single slash
|
|
13
|
-
let normalizedPath = path.replace(/\/+/g, '/')
|
|
14
|
-
|
|
15
|
-
// Then ensure there's a leading slash and no trailing slash (unless it's just /)
|
|
16
|
-
if (!normalizedPath.startsWith('/')) {
|
|
17
|
-
normalizedPath = `/${normalizedPath}`
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
|
|
21
|
-
normalizedPath = normalizedPath.slice(0, -1)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return normalizedPath
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Converts a string path to an action path for dynamic imports
|
|
29
|
-
* @param path The path to convert (e.g., 'Actions/Home/IndexAction')
|
|
30
|
-
*/
|
|
31
|
-
export function toActionPath(path: string): string {
|
|
32
|
-
return path.replace(/\//g, '_').toLowerCase()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Checks if a handler is a class constructor implementing ActionHandlerClass
|
|
37
|
-
* @param handler The handler to check
|
|
38
|
-
*/
|
|
39
|
-
export function isActionClass(handler: ActionHandler): handler is new () => ActionHandlerClass {
|
|
40
|
-
if (typeof handler !== 'function' && typeof handler !== 'object') {
|
|
41
|
-
return false
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check if it's a class constructor
|
|
45
|
-
if (typeof handler === 'function' && handler.prototype) {
|
|
46
|
-
const prototype = handler.prototype as unknown as { handle?: unknown }
|
|
47
|
-
if (typeof prototype.handle === 'function') {
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Check if it's an instance with a handle method
|
|
53
|
-
if (typeof handler === 'object' && handler !== null && 'handle' in handler && typeof (handler as any).handle === 'function') {
|
|
54
|
-
return true
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Checks if a handler is a route handler function
|
|
62
|
-
* @param handler The handler to check
|
|
63
|
-
*/
|
|
64
|
-
export function isRouteHandler(handler: ActionHandler): handler is RouteHandler {
|
|
65
|
-
return typeof handler === 'function' && !isActionClass(handler)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Extracts named parameters from a path pattern
|
|
70
|
-
* @param pattern The path pattern (e.g., '/users/{id}/posts/{postId}')
|
|
71
|
-
*/
|
|
72
|
-
export function extractParamNames(pattern: string): string[] {
|
|
73
|
-
const matches = pattern.match(/\{([^}]+)\}/g)
|
|
74
|
-
return matches ? matches.map(m => m.slice(1, -1).replace('?', '')) : []
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Creates a regex pattern from a path pattern
|
|
79
|
-
* @param pattern The path pattern
|
|
80
|
-
*/
|
|
81
|
-
export function createPathRegex(pattern: string): RegExp {
|
|
82
|
-
const regexPattern = pattern.replace(/\{([^}]+)\}/g, '([^/]+)')
|
|
83
|
-
return new RegExp(`^${regexPattern}$`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Per-pattern matching data. A route's pattern never changes, so the
|
|
88
|
-
* segment split, param-name extraction, and optional-param count are derived
|
|
89
|
-
* once and cached — turning `matchPath` from "regex + split on every request"
|
|
90
|
-
* into a tight array walk. Only the incoming `path` is processed per call.
|
|
91
|
-
*/
|
|
92
|
-
interface CompiledSegment {
|
|
93
|
-
isParam: boolean
|
|
94
|
-
paramName: string
|
|
95
|
-
isOptional: boolean
|
|
96
|
-
literal: string
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
interface CompiledPattern {
|
|
100
|
-
isWildcard: boolean
|
|
101
|
-
wildcardBase: string
|
|
102
|
-
segments: CompiledSegment[]
|
|
103
|
-
optionalCount: number
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/
|
|
107
|
-
const OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/
|
|
108
|
-
|
|
109
|
-
const compiledPatternCache = new Map<string, CompiledPattern>()
|
|
110
|
-
// Cached compiled constraint regexes; `null` marks an invalid pattern so we
|
|
111
|
-
// don't re-attempt construction on every request.
|
|
112
|
-
const constraintRegexCache = new Map<string, RegExp | null>()
|
|
113
|
-
|
|
114
|
-
function compilePattern(pattern: string): CompiledPattern {
|
|
115
|
-
const cached = compiledPatternCache.get(pattern)
|
|
116
|
-
if (cached)
|
|
117
|
-
return cached
|
|
118
|
-
|
|
119
|
-
const isWildcard = pattern.endsWith('/*')
|
|
120
|
-
const normalizedPattern = normalizePath(pattern)
|
|
121
|
-
let optionalCount = 0
|
|
122
|
-
const segments: CompiledSegment[] = normalizedPattern
|
|
123
|
-
.split('/')
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
.map((seg) => {
|
|
126
|
-
const isOptional = OPTIONAL_SEGMENT_RE.test(seg)
|
|
127
|
-
if (isOptional)
|
|
128
|
-
optionalCount++
|
|
129
|
-
const paramMatch = seg.match(PARAM_SEGMENT_RE)
|
|
130
|
-
return {
|
|
131
|
-
isParam: !!paramMatch,
|
|
132
|
-
paramName: paramMatch ? paramMatch[1].replace('?', '') : '',
|
|
133
|
-
isOptional,
|
|
134
|
-
literal: seg,
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const compiled: CompiledPattern = {
|
|
139
|
-
isWildcard,
|
|
140
|
-
wildcardBase: isWildcard ? pattern.slice(0, -2) : '',
|
|
141
|
-
segments,
|
|
142
|
-
optionalCount,
|
|
143
|
-
}
|
|
144
|
-
compiledPatternCache.set(pattern, compiled)
|
|
145
|
-
return compiled
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function getConstraintRegex(constraint: string): RegExp | null {
|
|
149
|
-
const cached = constraintRegexCache.get(constraint)
|
|
150
|
-
if (cached !== undefined)
|
|
151
|
-
return cached
|
|
152
|
-
let regex: RegExp | null
|
|
153
|
-
try {
|
|
154
|
-
regex = new RegExp(`^${constraint}$`)
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
regex = null
|
|
158
|
-
}
|
|
159
|
-
constraintRegexCache.set(constraint, regex)
|
|
160
|
-
return regex
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Matches a path against a pattern and extracts parameters
|
|
165
|
-
* @param pattern The path pattern (e.g., '/users/{id}')
|
|
166
|
-
* @param path The actual path (e.g., '/users/123')
|
|
167
|
-
* @param params Output parameter object that will contain extracted parameters
|
|
168
|
-
* @param constraints Optional constraints for route parameters
|
|
169
|
-
* @returns Boolean indicating whether the path matches the pattern
|
|
170
|
-
*/
|
|
171
|
-
export function matchPath(
|
|
172
|
-
pattern: string,
|
|
173
|
-
path: string,
|
|
174
|
-
params: Record<string, string>,
|
|
175
|
-
constraints?: Record<string, string>,
|
|
176
|
-
): boolean {
|
|
177
|
-
const compiled = compilePattern(pattern)
|
|
178
|
-
|
|
179
|
-
// Special case for wildcard patterns. Matches the original semantics:
|
|
180
|
-
// a prefix hit short-circuits to true, otherwise we fall through to the
|
|
181
|
-
// per-segment match below.
|
|
182
|
-
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
183
|
-
return true
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const patternSegments = compiled.segments
|
|
187
|
-
const pathSegments = normalizePath(path).split('/').filter(Boolean)
|
|
188
|
-
|
|
189
|
-
// Quick check - if segments don't match (accounting for optional params) then no match
|
|
190
|
-
// For mandatory parameters, the pattern and path must have same number of segments
|
|
191
|
-
if (pathSegments.length < patternSegments.length - compiled.optionalCount
|
|
192
|
-
|| pathSegments.length > patternSegments.length) {
|
|
193
|
-
return false
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Match each segment
|
|
197
|
-
for (let i = 0; i < patternSegments.length; i++) {
|
|
198
|
-
const seg = patternSegments[i]
|
|
199
|
-
const pathSegment = pathSegments[i]
|
|
200
|
-
|
|
201
|
-
// If we've run out of path segments and this is not an optional parameter
|
|
202
|
-
if (pathSegment === undefined) {
|
|
203
|
-
if (seg.isOptional) {
|
|
204
|
-
// This is an optional parameter and we have no path segment for it
|
|
205
|
-
continue
|
|
206
|
-
}
|
|
207
|
-
// Required parameter or static segment with no matching path segment
|
|
208
|
-
return false
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (seg.isParam) {
|
|
212
|
-
// Store the parameter value
|
|
213
|
-
params[seg.paramName] = pathSegment
|
|
214
|
-
|
|
215
|
-
// Check constraints if they exist
|
|
216
|
-
if (constraints && constraints[seg.paramName]) {
|
|
217
|
-
const regex = getConstraintRegex(constraints[seg.paramName])
|
|
218
|
-
if (regex) {
|
|
219
|
-
if (!regex.test(pathSegment)) {
|
|
220
|
-
return false
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
// If regex is invalid, treat as no constraint
|
|
225
|
-
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else if (seg.literal !== pathSegment) {
|
|
230
|
-
// Static segment doesn't match
|
|
231
|
-
return false
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return true
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Joins path segments ensuring proper formatting
|
|
240
|
-
* @param segments Path segments to join
|
|
241
|
-
*/
|
|
242
|
-
export function joinPaths(...segments: string[]): string {
|
|
243
|
-
return normalizePath(segments.join('/'))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Validates a route path format
|
|
248
|
-
* @param path The path to validate
|
|
249
|
-
*/
|
|
250
|
-
export function validatePath(path: string): boolean {
|
|
251
|
-
// Path must start with a slash
|
|
252
|
-
if (!path.startsWith('/')) {
|
|
253
|
-
return false
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Check for balanced curly braces
|
|
257
|
-
const stack: string[] = []
|
|
258
|
-
for (const char of path) {
|
|
259
|
-
if (char === '{') {
|
|
260
|
-
stack.push(char)
|
|
261
|
-
}
|
|
262
|
-
else if (char === '}') {
|
|
263
|
-
if (stack.length === 0 || stack.pop() !== '{') {
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return stack.length === 0
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Check if a file exists at the given path
|
|
274
|
-
* @param path The file path to check
|
|
275
|
-
*/
|
|
276
|
-
export async function fileExists(path: string): Promise<boolean> {
|
|
277
|
-
try {
|
|
278
|
-
const file = Bun.file(path)
|
|
279
|
-
return await file.exists()
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
return false
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Resolve a view name to an actual file path
|
|
288
|
-
* @param viewName The view name (without extension)
|
|
289
|
-
* @param viewsPath The base path for views
|
|
290
|
-
* @param extensions File extensions to try
|
|
291
|
-
*/
|
|
292
|
-
export async function resolveViewPath(
|
|
293
|
-
viewName: string,
|
|
294
|
-
viewsPath: string,
|
|
295
|
-
extensions: string[],
|
|
296
|
-
): Promise<string | null> {
|
|
297
|
-
// If viewName already contains an extension, try it directly
|
|
298
|
-
if (extensions.some(ext => viewName.endsWith(ext))) {
|
|
299
|
-
const fullPath = normalizePath(join(viewsPath, viewName))
|
|
300
|
-
if (await fileExists(fullPath)) {
|
|
301
|
-
return fullPath
|
|
302
|
-
}
|
|
303
|
-
return null
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Try each extension
|
|
307
|
-
for (const ext of extensions) {
|
|
308
|
-
const fullPath = normalizePath(join(viewsPath, `${viewName}${ext}`))
|
|
309
|
-
if (await fileExists(fullPath)) {
|
|
310
|
-
return fullPath
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Basic HTML template processing - replaces {{ varName }} with the corresponding value
|
|
319
|
-
* @param template The HTML template string
|
|
320
|
-
* @param data The data to inject into the template
|
|
321
|
-
*/
|
|
322
|
-
export function processHtmlTemplate(template: string, data: Record<string, unknown>): string {
|
|
323
|
-
// Process general variables {{ varName }}
|
|
324
|
-
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
325
|
-
const trimmedKey = key.trim()
|
|
326
|
-
const value = getNestedValue(data, trimmedKey)
|
|
327
|
-
return value !== undefined ? String(value) : match
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
// Process conditionals {{#if condition}} content {{/if}}
|
|
331
|
-
result = processConditionals(result, data)
|
|
332
|
-
|
|
333
|
-
// Process loops {{#each items}} content {{/each}}
|
|
334
|
-
result = processLoops(result, data)
|
|
335
|
-
|
|
336
|
-
return result
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Process conditional statements in templates
|
|
341
|
-
*/
|
|
342
|
-
function processConditionals(template: string, data: Record<string, unknown>): string {
|
|
343
|
-
// Match {{#if condition}} content {{/if}} or {{#if condition}} content {{else}} alternative {{/if}}
|
|
344
|
-
return template.replace(
|
|
345
|
-
/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
|
|
346
|
-
(_match, condition, content, alternative = '') => {
|
|
347
|
-
const conditionTrimmed = condition.trim()
|
|
348
|
-
const value = getNestedValue(data, conditionTrimmed)
|
|
349
|
-
// Explicitly check for falsy values (false, undefined, null, 0, "")
|
|
350
|
-
return value ? content : alternative
|
|
351
|
-
},
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Process loop statements in templates
|
|
357
|
-
*/
|
|
358
|
-
function processLoops(template: string, data: Record<string, unknown>): string {
|
|
359
|
-
// Match {{#each items}} content {{/each}}
|
|
360
|
-
return template.replace(
|
|
361
|
-
/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
362
|
-
(_match, arrayKey, content) => {
|
|
363
|
-
const array = getNestedValue(data, arrayKey.trim())
|
|
364
|
-
if (!Array.isArray(array))
|
|
365
|
-
return ''
|
|
366
|
-
|
|
367
|
-
return array.map((item) => {
|
|
368
|
-
// Replace {{@index}} with the current index
|
|
369
|
-
// Replace {{this}} with the current item
|
|
370
|
-
// Replace {{this.property}} with the current item's property
|
|
371
|
-
return content
|
|
372
|
-
.replace(/\{\{@index\}\}/g, String(array.indexOf(item)))
|
|
373
|
-
.replace(/\{\{this\}\}/g, String(item))
|
|
374
|
-
.replace(/\{\{this\.([^}]+)\}\}/g, (_matchStr: string, prop: string) => {
|
|
375
|
-
const value = item[prop.trim()]
|
|
376
|
-
return value !== undefined ? String(value) : ''
|
|
377
|
-
})
|
|
378
|
-
}).join('')
|
|
379
|
-
},
|
|
380
|
-
)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Get a nested value from an object using dot notation
|
|
385
|
-
* @param obj The object to extract value from
|
|
386
|
-
* @param path The path to the value (e.g., 'user.profile.name')
|
|
387
|
-
*/
|
|
388
|
-
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
389
|
-
return path.split('.').reduce<unknown>((prev, curr) => {
|
|
390
|
-
if (prev && typeof prev === 'object' && curr in prev) {
|
|
391
|
-
return (prev as Record<string, unknown>)[curr]
|
|
392
|
-
}
|
|
393
|
-
return undefined
|
|
394
|
-
}, obj)
|
|
395
|
-
}
|