rari 0.1.3

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.
@@ -0,0 +1,473 @@
1
+ import type {
2
+ FileRouteInfo,
3
+ Route,
4
+ RouteMatch,
5
+ RouteParams,
6
+ SearchParams,
7
+ } from './types'
8
+
9
+ export function filePathToRoutePath(filePath: string): string {
10
+ let routePath = filePath
11
+ .replace(/^pages\//, '')
12
+ .replace(/\.(tsx?|jsx?)$/, '')
13
+
14
+ if (routePath === 'index') {
15
+ routePath = '/'
16
+ }
17
+ else if (routePath.endsWith('/index')) {
18
+ routePath = routePath.replace(/\/index$/, '')
19
+ }
20
+
21
+ routePath = routePath.replace(/\[([^\]]+)\]/g, (match, param) => {
22
+ if (param.startsWith('...')) {
23
+ return `:${param.slice(3)}*`
24
+ }
25
+ return `:${param}`
26
+ })
27
+
28
+ if (!routePath.startsWith('/')) {
29
+ routePath = `/${routePath}`
30
+ }
31
+
32
+ return routePath
33
+ }
34
+
35
+ export function extractParamNames(routePath: string): string[] {
36
+ const params: string[] = []
37
+ const paramRegex = /:([^/]+)/g
38
+
39
+ const matches = Array.from(routePath.matchAll(paramRegex))
40
+
41
+ for (const match of matches) {
42
+ let paramName = match[1]
43
+
44
+ if (paramName.endsWith('*')) {
45
+ paramName = paramName.slice(0, -1)
46
+ }
47
+
48
+ params.push(paramName)
49
+ }
50
+
51
+ return params
52
+ }
53
+
54
+ export function isDynamicRoute(routePath: string): boolean {
55
+ return routePath.includes(':')
56
+ }
57
+
58
+ export function routePathToRegex(routePath: string): RegExp {
59
+ let pattern = routePath.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
60
+
61
+ pattern = pattern.replace(/:([^/]+)/g, (match, paramName) => {
62
+ if (paramName.endsWith('*')) {
63
+ return '(.*)'
64
+ }
65
+ return '([^/]+)'
66
+ })
67
+
68
+ pattern = `^${pattern}$`
69
+
70
+ return new RegExp(pattern)
71
+ }
72
+
73
+ export function matchRoute(pathname: string, route: Route): RouteMatch | null {
74
+ const regex = routePathToRegex(route.path)
75
+ const match = pathname.match(regex)
76
+
77
+ if (!match) {
78
+ return null
79
+ }
80
+
81
+ const params: RouteParams = {}
82
+ const paramNames = route.paramNames || []
83
+
84
+ for (let i = 0; i < paramNames.length; i++) {
85
+ const paramName = paramNames[i]
86
+ const paramValue = match[i + 1]
87
+
88
+ if (paramValue !== undefined) {
89
+ if (route.path.includes(`:${paramName}*`)) {
90
+ const segments = paramValue.split('/').filter(Boolean)
91
+ params[paramName] = segments
92
+ }
93
+ else {
94
+ params[paramName] = decodeURIComponent(paramValue)
95
+ }
96
+ }
97
+ }
98
+
99
+ return {
100
+ route,
101
+ params,
102
+ searchParams: {},
103
+ pathname,
104
+ search: '',
105
+ hash: '',
106
+ }
107
+ }
108
+
109
+ export function findMatchingRoute(
110
+ pathname: string,
111
+ routes: Route[],
112
+ ): RouteMatch | null {
113
+ const routeHierarchy = buildRouteHierarchy(routes)
114
+
115
+ const match = findNestedRouteMatch(pathname, routeHierarchy)
116
+
117
+ if (!match) {
118
+ return null
119
+ }
120
+
121
+ return {
122
+ route: match.route,
123
+ params: match.params,
124
+ searchParams: {},
125
+ pathname,
126
+ search: '',
127
+ hash: '',
128
+ parentMatches: match.parentMatches,
129
+ layouts: match.layouts,
130
+ }
131
+ }
132
+
133
+ interface NestedRouteMatch {
134
+ route: Route
135
+ params: RouteParams
136
+ parentMatches: RouteMatch[]
137
+ layouts: Route[]
138
+ }
139
+
140
+ function buildRouteHierarchy(routes: Route[]): Map<string, Route> {
141
+ const routeMap = new Map<string, Route>()
142
+
143
+ for (const route of routes) {
144
+ routeMap.set(route.path, route)
145
+ }
146
+
147
+ return routeMap
148
+ }
149
+
150
+ function findNestedRouteMatch(
151
+ pathname: string,
152
+ routeHierarchy: Map<string, Route>,
153
+ ): NestedRouteMatch | null {
154
+ let bestMatch: NestedRouteMatch | null = null
155
+ let highestPriority = -Infinity
156
+
157
+ for (const route of routeHierarchy.values()) {
158
+ const match = matchRouteWithHierarchy(pathname, route, routeHierarchy)
159
+
160
+ if (match) {
161
+ const priority = getRoutePriority(route)
162
+ if (priority > highestPriority) {
163
+ bestMatch = match
164
+ highestPriority = priority
165
+ }
166
+ }
167
+ }
168
+
169
+ return bestMatch
170
+ }
171
+
172
+ function matchRouteWithHierarchy(
173
+ pathname: string,
174
+ route: Route,
175
+ routeHierarchy: Map<string, Route>,
176
+ ): NestedRouteMatch | null {
177
+ const routeMatch = matchRoute(pathname, route)
178
+
179
+ if (!routeMatch) {
180
+ return null
181
+ }
182
+
183
+ const parentMatches: RouteMatch[] = []
184
+ const layouts: Route[] = []
185
+
186
+ let currentRoute = route.parent
187
+ while (currentRoute) {
188
+ parentMatches.unshift({
189
+ route: currentRoute,
190
+ params: {},
191
+ searchParams: {},
192
+ pathname: currentRoute.path,
193
+ search: '',
194
+ hash: '',
195
+ })
196
+
197
+ const layoutRoute = findLayoutForRoute(currentRoute, routeHierarchy)
198
+ if (layoutRoute) {
199
+ layouts.unshift(layoutRoute)
200
+ }
201
+
202
+ currentRoute = currentRoute.parent
203
+ }
204
+
205
+ const matchedRouteLayout = findLayoutForRoute(route, routeHierarchy)
206
+ if (matchedRouteLayout) {
207
+ layouts.push(matchedRouteLayout)
208
+ }
209
+
210
+ return {
211
+ route,
212
+ params: routeMatch.params,
213
+ parentMatches,
214
+ layouts,
215
+ }
216
+ }
217
+
218
+ function findLayoutForRoute(route: Route, routeHierarchy: Map<string, Route>): Route | null {
219
+ const routeDir = route.filePath.split('/').slice(0, -1).join('/')
220
+ const possibleLayoutPaths = [
221
+ `${routeDir}/layout.tsx`,
222
+ `${routeDir}/layout.jsx`,
223
+ `${routeDir}/_layout.tsx`,
224
+ `${routeDir}/_layout.jsx`,
225
+ ]
226
+
227
+ for (const layoutPath of possibleLayoutPaths) {
228
+ for (const candidateRoute of routeHierarchy.values()) {
229
+ if (candidateRoute.filePath === layoutPath && candidateRoute.isLayout) {
230
+ return candidateRoute
231
+ }
232
+ }
233
+ }
234
+
235
+ return null
236
+ }
237
+
238
+ export function parseSearchParams(search: string): SearchParams {
239
+ const params: SearchParams = {}
240
+
241
+ if (!search || search === '?') {
242
+ return params
243
+ }
244
+
245
+ const searchParams = new URLSearchParams(
246
+ search.startsWith('?') ? search.slice(1) : search,
247
+ )
248
+
249
+ for (const [key, value] of searchParams.entries()) {
250
+ if (params[key]) {
251
+ if (Array.isArray(params[key])) {
252
+ (params[key] as string[]).push(value)
253
+ }
254
+ else {
255
+ params[key] = [params[key] as string, value]
256
+ }
257
+ }
258
+ else {
259
+ params[key] = value
260
+ }
261
+ }
262
+
263
+ return params
264
+ }
265
+
266
+ export function buildSearchString(params: SearchParams): string {
267
+ const searchParams = new URLSearchParams()
268
+
269
+ for (const [key, value] of Object.entries(params)) {
270
+ if (Array.isArray(value)) {
271
+ value.forEach(v => searchParams.append(key, v))
272
+ }
273
+ else {
274
+ searchParams.set(key, value)
275
+ }
276
+ }
277
+
278
+ const search = searchParams.toString()
279
+ return search ? `?${search}` : ''
280
+ }
281
+
282
+ export function parseUrl(url: string): {
283
+ pathname: string
284
+ search: string
285
+ hash: string
286
+ searchParams: SearchParams
287
+ } {
288
+ try {
289
+ const parsed = new URL(url, 'http://localhost')
290
+
291
+ return {
292
+ pathname: parsed.pathname,
293
+ search: parsed.search,
294
+ hash: parsed.hash,
295
+ searchParams: parseSearchParams(parsed.search),
296
+ }
297
+ }
298
+ catch {
299
+ const [pathname, rest] = url.split('?', 2)
300
+ const [search, hash] = rest ? rest.split('#', 2) : ['', '']
301
+
302
+ return {
303
+ pathname: pathname || '/',
304
+ search: search ? `?${search}` : '',
305
+ hash: hash ? `#${hash}` : '',
306
+ searchParams: parseSearchParams(search || ''),
307
+ }
308
+ }
309
+ }
310
+
311
+ export function buildUrl(
312
+ pathname: string,
313
+ searchParams?: SearchParams,
314
+ hash?: string,
315
+ ): string {
316
+ let url = pathname
317
+
318
+ if (searchParams) {
319
+ const search = buildSearchString(searchParams)
320
+ if (search) {
321
+ url += search
322
+ }
323
+ }
324
+
325
+ if (hash) {
326
+ url += hash.startsWith('#') ? hash : `#${hash}`
327
+ }
328
+
329
+ return url
330
+ }
331
+
332
+ export function analyzeFilePath(filePath: string): FileRouteInfo {
333
+ const routePath = filePathToRoutePath(filePath)
334
+ const isDynamic = isDynamicRoute(routePath)
335
+ const paramNames = extractParamNames(routePath)
336
+
337
+ const fileName = filePath.split('/').pop() || ''
338
+
339
+ return {
340
+ filePath,
341
+ routePath,
342
+ isDynamic,
343
+ paramNames,
344
+ isIndex:
345
+ fileName === 'index.tsx'
346
+ || fileName === 'index.jsx'
347
+ || fileName === 'index.ts'
348
+ || fileName === 'index.js'
349
+ || filePath.endsWith('/index.tsx')
350
+ || filePath.endsWith('/index.jsx')
351
+ || filePath === 'pages/index.tsx'
352
+ || filePath === 'pages/index.jsx',
353
+ isLayout:
354
+ fileName === 'layout.tsx'
355
+ || fileName === 'layout.jsx'
356
+ || fileName === '_layout.tsx'
357
+ || fileName === '_layout.jsx'
358
+ || filePath.includes('/layout.')
359
+ || filePath.includes('/_layout.'),
360
+ isNotFound: filePath.includes('404.') || filePath.includes('_error.'),
361
+ }
362
+ }
363
+
364
+ export function sortRoutesBySpecificity(routes: Route[]): Route[] {
365
+ return [...routes].sort((a, b) => {
366
+ if (!a.isDynamic && b.isDynamic)
367
+ return -1
368
+ if (a.isDynamic && !b.isDynamic)
369
+ return 1
370
+
371
+ const aSegments = a.path.split('/').length
372
+ const bSegments = b.path.split('/').length
373
+
374
+ if (aSegments !== bSegments) {
375
+ return aSegments - bSegments
376
+ }
377
+
378
+ const aParamCount = a.paramNames?.length || 0
379
+ const bParamCount = b.paramNames?.length || 0
380
+
381
+ if (aParamCount !== bParamCount) {
382
+ return aParamCount - bParamCount
383
+ }
384
+
385
+ const aHasCatchAll = a.path.includes('*')
386
+ const bHasCatchAll = b.path.includes('*')
387
+
388
+ if (aHasCatchAll && !bHasCatchAll)
389
+ return 1
390
+ if (!aHasCatchAll && bHasCatchAll)
391
+ return -1
392
+
393
+ return 0
394
+ })
395
+ }
396
+
397
+ export function routePathsEqual(a: string, b: string): boolean {
398
+ return a === b
399
+ }
400
+
401
+ export function isPathActive(
402
+ pathname: string,
403
+ routePath: string,
404
+ exact: boolean = false,
405
+ ): boolean {
406
+ if (exact) {
407
+ return pathname === routePath
408
+ }
409
+
410
+ if (routePath === '/') {
411
+ return pathname === '/'
412
+ }
413
+
414
+ return pathname === routePath || pathname.startsWith(`${routePath}/`)
415
+ }
416
+
417
+ export function normalizePathname(pathname: string): string {
418
+ if (pathname === '/' || pathname === '') {
419
+ return '/'
420
+ }
421
+
422
+ return pathname.replace(/\/+$/, '')
423
+ }
424
+
425
+ export function joinPaths(...segments: string[]): string {
426
+ return (
427
+ segments
428
+ .filter(Boolean)
429
+ .join('/')
430
+ .replace(/\/+/g, '/')
431
+ .replace(/\/$/, '') || '/'
432
+ )
433
+ }
434
+
435
+ export function getParentPath(pathname: string): string {
436
+ const segments = pathname.split('/').filter(Boolean)
437
+ if (segments.length <= 1) {
438
+ return '/'
439
+ }
440
+
441
+ return `/${segments.slice(0, -1).join('/')}`
442
+ }
443
+
444
+ export function getParentPaths(pathname: string): string[] {
445
+ const segments = pathname.split('/').filter(Boolean)
446
+ const parents: string[] = ['/']
447
+
448
+ for (let i = 1; i < segments.length; i++) {
449
+ parents.push(`/${segments.slice(0, i).join('/')}`)
450
+ }
451
+
452
+ return parents
453
+ }
454
+
455
+ export function getRoutePriority(route: Route): number {
456
+ let priority = 0
457
+
458
+ if (!route.isDynamic) {
459
+ priority += 1000
460
+ }
461
+
462
+ const paramCount = route.paramNames?.length || 0
463
+ priority -= paramCount * 100
464
+
465
+ if (route.path.includes('*')) {
466
+ priority -= 500
467
+ }
468
+
469
+ const segmentCount = route.path.split('/').length
470
+ priority += segmentCount * 10
471
+
472
+ return priority
473
+ }