safa-router 1.1.1 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safa-router",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "A professional standalone frontend router inspired by Next.js App Router — works with any framework or vanilla JS",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/Link.js CHANGED
@@ -73,7 +73,7 @@ export class Link {
73
73
  const cur = this._router.pathname
74
74
  const active =
75
75
  cur === this._href ||
76
- (this._href !== '/' && cur.startsWith(this._href))
76
+ (this._href !== '/' && (cur.startsWith(this._href + '/') || cur === this._href))
77
77
  this._el.classList.toggle(this._activeClass, active)
78
78
  }
79
79
 
@@ -100,9 +100,9 @@ class RoutePattern {
100
100
  let path = this.raw
101
101
  for (const [key, val] of Object.entries(params)) {
102
102
  const v = Array.isArray(val) ? val.join('/') : String(val)
103
- path = path.replace(`[[...${key}]]`, v)
104
- path = path.replace(`[...${key}]`, v)
105
- path = path.replace(`[${key}]`, v)
103
+ path = path.replaceAll(`[[...${key}]]`, v)
104
+ path = path.replaceAll(`[...${key}]`, v)
105
+ path = path.replaceAll(`[${key}]`, v)
106
106
  }
107
107
  return normalizePath(path)
108
108
  }
package/src/SafaRouter.js CHANGED
@@ -1,4 +1,3 @@
1
- import { RouteMatcher } from './RouteMatcher.js'
2
1
  import { RouteTree } from './RouteTree.js'
3
2
  import { HistoryManager } from './HistoryManager.js'
4
3
  import { MiddlewareChain } from './MiddlewareChain.js'
@@ -8,8 +7,8 @@ import { EVENTS, DEFAULT_CONFIG } from './constants.js'
8
7
  import { RouteLoadError, SafaError } from './errors.js'
9
8
 
10
9
  export class SafaRouter {
11
- static version = '1.1.0'
12
- static VERSION = '1.1.0'
10
+ static version = '1.1.2'
11
+ static VERSION = '1.1.2'
13
12
 
14
13
  constructor(options = {}) {
15
14
  this.config = { ...DEFAULT_CONFIG, ...options }
@@ -20,7 +19,6 @@ export class SafaRouter {
20
19
  this._events = {}
21
20
  for (const key of Object.values(EVENTS)) this._events[key] = []
22
21
 
23
- this._matcher = new RouteMatcher()
24
22
  this._history = new HistoryManager({
25
23
  useHash: this.config.useHash,
26
24
  basePath: this.config.basePath,
@@ -36,6 +34,7 @@ export class SafaRouter {
36
34
  this._isLoading = false
37
35
  this._started = false
38
36
  this._targetEl = null
37
+ this._navId = 0
39
38
 
40
39
  this._globalNotFound = this.config.notFound || null
41
40
  this._globalError = this.config.error || null
@@ -62,7 +61,6 @@ export class SafaRouter {
62
61
  }
63
62
 
64
63
  this._routeTree = new RouteTree(this.config.routes || {})
65
- this._seedMatcher()
66
64
  this._history.init()
67
65
  this._unsubHistory = this._history.onChange(this._boundNav)
68
66
  await this._resolve(this._history.path, 'replace')
@@ -145,23 +143,6 @@ export class SafaRouter {
145
143
 
146
144
  clearCache() { this._cache.clear() }
147
145
 
148
- _seedMatcher() {
149
- const walk = (routes, base) => {
150
- if (!routes || typeof routes !== 'object') return
151
- for (const [key, val] of Object.entries(routes)) {
152
- const isGroup = key.startsWith('(') && key.endsWith(')')
153
- const fp = isGroup ? base : base === '/' ? `/${key}` : `${base}/${key}`
154
- if (typeof val === 'object' && val !== null) {
155
- if (val.page) this._matcher.add(fp)
156
- if (val.children) walk(val.children, fp)
157
- } else if (typeof val === 'function') {
158
- this._matcher.add(fp)
159
- }
160
- }
161
- }
162
- walk(this.config.routes || {}, '/')
163
- }
164
-
165
146
  _resolvePagePath(path) {
166
147
  const dir = (this.config.pagesDir || '').replace(/\/+$/, '')
167
148
  if (!dir) return null
@@ -236,12 +217,14 @@ export class SafaRouter {
236
217
  return html.replace(/\{\s*children\s*\}/gi, children || '')
237
218
  }
238
219
 
239
- async _navigate(path, method, query = {}, state = {}) {
240
- if (path === this._pathname) return
241
- await this._resolve(path, method, query, state)
220
+ async _navigate(path, method, query = {}, state = {}, depth = 0) {
221
+ if (depth > 10) { console.error('[SafaRouter] Redirect loop detected'); return }
222
+ if (path === this._pathname && depth === 0) return
223
+ await this._resolve(path, method, query, state, depth)
242
224
  }
243
225
 
244
- async _resolve(path, method, query = {}, state = {}) {
226
+ async _resolve(path, method, query = {}, state = {}, depth = 0) {
227
+ const navId = ++this._navId
245
228
  this._isLoading = true
246
229
  this._customTitle = null
247
230
  emit(this._events, EVENTS.LOADING, { path, loading: true })
@@ -250,7 +233,8 @@ export class SafaRouter {
250
233
  try {
251
234
  const ctx = { path, method, query, cancelled: false, redirect: null }
252
235
  await this._middleware.run(ctx)
253
- if (ctx.redirect) return this._navigate(ctx.redirect, 'replace')
236
+ if (this._navId !== navId) return
237
+ if (ctx.redirect) return this._navigate(ctx.redirect, 'replace', {}, {}, depth + 1)
254
238
  if (ctx.cancelled) { this._isLoading = false; return }
255
239
 
256
240
  const routeMatch = this._hasRoutes() ? this._routeTree.resolve(path) : null
@@ -315,6 +299,8 @@ export class SafaRouter {
315
299
  return
316
300
  }
317
301
 
302
+ if (this._navId !== navId) return
303
+
318
304
  this._saveScroll()
319
305
 
320
306
  if (method === 'push') this._history.push(path, state)
@@ -327,14 +313,14 @@ export class SafaRouter {
327
313
 
328
314
  this._render(pageContent, layoutFns)
329
315
 
316
+ this._restoreScroll()
317
+ this._updateTitle()
318
+ this._focus()
319
+
330
320
  emit(this._events, EVENTS.ROUTE_CHANGE, {
331
321
  pathname: path, params: this._params, query: this._query,
332
322
  })
333
323
  emit(this._events, EVENTS.AFTER_NAVIGATE, { pathname: path })
334
-
335
- this._updateTitle()
336
- this._restoreScroll()
337
- this._focus()
338
324
  } catch (err) {
339
325
  this._isLoading = false
340
326
  await this._handleError(path, err)
@@ -442,15 +428,7 @@ export class SafaRouter {
442
428
  this._extractTitle(text)
443
429
  return text
444
430
  }
445
- if (typeof mod === 'function') {
446
- let result
447
- try { result = mod() } catch { return mod }
448
- if (result && typeof result.then === 'function') {
449
- const resolved = await result
450
- return resolved && resolved.default ? resolved.default : resolved
451
- }
452
- return result !== undefined ? result : mod
453
- }
431
+ if (typeof mod === 'function') return mod
454
432
  return mod
455
433
  } catch (e) {
456
434
  throw new RouteLoadError(
@@ -538,7 +516,6 @@ export class SafaRouter {
538
516
  }
539
517
 
540
518
  get currentRoute() { return this._routeData }
541
- get matchedRoute() { return this._matcher.match(this._pathname) }
542
519
 
543
520
  getRoute(path) { return this._routeTree.resolve(normalizePath(path)) }
544
521
 
package/src/constants.js CHANGED
@@ -23,7 +23,7 @@ export const SEGMENT_TYPES = {
23
23
  export const PARAM_PATTERNS = {
24
24
  DYNAMIC: /^\[([^\]]+)\]$/,
25
25
  CATCH_ALL: /^\[\.\.\.([^\]]+)\]$/,
26
- OPTIONAL_CATCH_ALL: /^\[\[\.\.\.[^\]]+\]\]$/,
26
+ OPTIONAL_CATCH_ALL: /^\[\[\.\.\.([^\]]+)\]\]$/,
27
27
  GROUP: /^\(([^)]+)\)$/,
28
28
  }
29
29