safa-router 1.0.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.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/package.json +43 -0
- package/src/HistoryManager.js +95 -0
- package/src/Link.js +97 -0
- package/src/MiddlewareChain.js +47 -0
- package/src/RouteMatcher.js +156 -0
- package/src/RouteTree.js +187 -0
- package/src/SafaRouter.js +464 -0
- package/src/constants.js +47 -0
- package/src/errors.js +46 -0
- package/src/index.js +16 -0
- package/src/link-helper.js +24 -0
- package/src/utils.js +122 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class HistoryManager {
|
|
2
|
+
constructor({ useHash = false, basePath = '' } = {}) {
|
|
3
|
+
this._useHash = useHash
|
|
4
|
+
this._basePath = basePath
|
|
5
|
+
this._listeners = []
|
|
6
|
+
this._bound = this._onPop.bind(this)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
init() {
|
|
10
|
+
window.addEventListener('popstate', this._bound)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
destroy() {
|
|
14
|
+
window.removeEventListener('popstate', this._bound)
|
|
15
|
+
this._listeners = []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isSupported() {
|
|
19
|
+
return typeof window !== 'undefined' && !!window.history
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get path() {
|
|
23
|
+
if (this._useHash) {
|
|
24
|
+
const h = location.hash.slice(1)
|
|
25
|
+
return h || '/'
|
|
26
|
+
}
|
|
27
|
+
let p = location.pathname
|
|
28
|
+
if (this._basePath && p.startsWith(this._basePath)) {
|
|
29
|
+
p = p.slice(this._basePath.length) || '/'
|
|
30
|
+
}
|
|
31
|
+
return p
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
push(url, state = {}) {
|
|
35
|
+
const scrollY = window.scrollY
|
|
36
|
+
history.pushState({ ...state, _scrollY: scrollY }, '', this._url(url))
|
|
37
|
+
this._notify(url, 'push')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
replace(url, state = {}) {
|
|
41
|
+
const scrollY = window.scrollY
|
|
42
|
+
history.replaceState({ ...state, _scrollY: scrollY }, '', this._url(url))
|
|
43
|
+
this._notify(url, 'replace')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
back() {
|
|
47
|
+
history.back()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
forward() {
|
|
51
|
+
history.forward()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
go(delta) {
|
|
55
|
+
history.go(delta)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get length() {
|
|
59
|
+
return history.length
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get state() {
|
|
63
|
+
return history.state
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clearState() {
|
|
67
|
+
history.replaceState(null, '', location.href)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onChange(fn) {
|
|
71
|
+
this._listeners.push(fn)
|
|
72
|
+
return () => {
|
|
73
|
+
this._listeners = this._listeners.filter(l => l !== fn)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_url(path) {
|
|
78
|
+
if (this._useHash) return `#${path}`
|
|
79
|
+
return this._basePath ? `${this._basePath}${path}` : path
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_onPop(e) {
|
|
83
|
+
this._notify(this.path, 'popstate', e.state)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_notify(path, action, state) {
|
|
87
|
+
for (const fn of this._listeners) {
|
|
88
|
+
try {
|
|
89
|
+
fn({ path, action, state })
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[SafaRouter] History listener error:', err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/Link.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class Link {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
this._href = config.href || '/'
|
|
4
|
+
this._children = config.children || this._href
|
|
5
|
+
this._className = config.className || ''
|
|
6
|
+
this._activeClass = config.activeClass || 'active'
|
|
7
|
+
this._router = config.router || null
|
|
8
|
+
this._attrs = config.attrs || {}
|
|
9
|
+
this._el = null
|
|
10
|
+
this._unsub = null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get href() { return this._href }
|
|
14
|
+
get element() { return this._el }
|
|
15
|
+
|
|
16
|
+
render(container) {
|
|
17
|
+
this._el = document.createElement('a')
|
|
18
|
+
if (container) {
|
|
19
|
+
if (typeof container === 'string') {
|
|
20
|
+
const el = document.querySelector(container)
|
|
21
|
+
if (el) el.appendChild(this._el)
|
|
22
|
+
} else if (container instanceof Node) {
|
|
23
|
+
container.appendChild(this._el)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this._el.href = this._router?.config?.useHash
|
|
27
|
+
? `#${this._href}`
|
|
28
|
+
: this._href
|
|
29
|
+
if (this._className) this._el.className = this._className
|
|
30
|
+
|
|
31
|
+
if (typeof this._children === 'string') {
|
|
32
|
+
this._el.textContent = this._children
|
|
33
|
+
} else if (this._children instanceof Node) {
|
|
34
|
+
this._el.appendChild(this._children)
|
|
35
|
+
} else if (Array.isArray(this._children)) {
|
|
36
|
+
for (const c of this._children) {
|
|
37
|
+
if (c instanceof Node) this._el.appendChild(c)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const [k, v] of Object.entries(this._attrs)) {
|
|
42
|
+
this._el.setAttribute(k, v)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._el.addEventListener('click', (e) => this._onClick(e))
|
|
46
|
+
|
|
47
|
+
if (this._router) {
|
|
48
|
+
if (this._router.config.prefetch) {
|
|
49
|
+
this._el.addEventListener('mouseenter', () => this._prefetch(), { once: true })
|
|
50
|
+
}
|
|
51
|
+
this._unsub = this._router.on('routechange', () => this._refresh())
|
|
52
|
+
this._refresh()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this._el
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_prefetch() {
|
|
59
|
+
if (this._router && this._router.prefetch) {
|
|
60
|
+
this._router.prefetch(this._href)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_onClick(e) {
|
|
65
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
66
|
+
if (e.button !== 0) return
|
|
67
|
+
e.preventDefault()
|
|
68
|
+
if (this._router) this._router.push(this._href)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_refresh() {
|
|
72
|
+
if (!this._el || !this._router) return
|
|
73
|
+
const cur = this._router.pathname
|
|
74
|
+
const active =
|
|
75
|
+
cur === this._href ||
|
|
76
|
+
(this._href !== '/' && cur.startsWith(this._href))
|
|
77
|
+
this._el.classList.toggle(this._activeClass, active)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setHref(href) {
|
|
81
|
+
this._href = href
|
|
82
|
+
if (this._el) {
|
|
83
|
+
this._el.href = this._router?.config?.useHash ? `#${href}` : href
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this._unsub) {
|
|
89
|
+
this._unsub()
|
|
90
|
+
this._unsub = null
|
|
91
|
+
}
|
|
92
|
+
if (this._el) {
|
|
93
|
+
this._el.remove()
|
|
94
|
+
this._el = null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class MiddlewareChain {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._stack = []
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
use(fn) {
|
|
7
|
+
if (typeof fn !== 'function') {
|
|
8
|
+
throw new Error('Middleware must be a function')
|
|
9
|
+
}
|
|
10
|
+
this._stack.push(fn)
|
|
11
|
+
return this
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run(ctx) {
|
|
15
|
+
let i = 0
|
|
16
|
+
const next = async () => {
|
|
17
|
+
if (i >= this._stack.length) return ctx
|
|
18
|
+
try {
|
|
19
|
+
return this._stack[i++](ctx, next)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('[SafaRouter] Middleware error:', err)
|
|
22
|
+
throw err
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return next()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear() {
|
|
29
|
+
this._stack = []
|
|
30
|
+
return this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
remove(fn) {
|
|
34
|
+
this._stack = this._stack.filter(f => f !== fn)
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clone() {
|
|
39
|
+
const chain = new MiddlewareChain()
|
|
40
|
+
chain._stack = [...this._stack]
|
|
41
|
+
return chain
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get length() {
|
|
45
|
+
return this._stack.length
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { normalizePath } from './utils.js'
|
|
2
|
+
import { PARAM_PATTERNS, SEGMENT_TYPES, PATTERN_SCORES } from './constants.js'
|
|
3
|
+
|
|
4
|
+
class RoutePattern {
|
|
5
|
+
constructor(pattern) {
|
|
6
|
+
this.raw = pattern
|
|
7
|
+
this.path = normalizePath(pattern)
|
|
8
|
+
this.segments = this.path === '/' ? [] : this.path.split('/').filter(Boolean)
|
|
9
|
+
this.paramNames = []
|
|
10
|
+
this._validate()
|
|
11
|
+
this._compile()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_validate() {
|
|
15
|
+
const names = new Set()
|
|
16
|
+
for (const seg of this.segments) {
|
|
17
|
+
const m = seg.match(/^\[(?:\.\.\.)?([^\]]+)\]$/)
|
|
18
|
+
if (m) {
|
|
19
|
+
if (names.has(m[1])) {
|
|
20
|
+
throw new Error(`Duplicate param name "${m[1]}" in pattern "${this.raw}"`)
|
|
21
|
+
}
|
|
22
|
+
names.add(m[1])
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_compile() {
|
|
28
|
+
let regexStr = '^'
|
|
29
|
+
|
|
30
|
+
for (const seg of this.segments) {
|
|
31
|
+
if (PARAM_PATTERNS.OPTIONAL_CATCH_ALL.test(seg)) {
|
|
32
|
+
const name = seg.match(PARAM_PATTERNS.OPTIONAL_CATCH_ALL)[1]
|
|
33
|
+
this.paramNames.push(name)
|
|
34
|
+
regexStr += '(?:/(.*))?'
|
|
35
|
+
} else if (PARAM_PATTERNS.CATCH_ALL.test(seg)) {
|
|
36
|
+
const name = seg.match(PARAM_PATTERNS.CATCH_ALL)[1]
|
|
37
|
+
this.paramNames.push(name)
|
|
38
|
+
regexStr += '/(.+)'
|
|
39
|
+
} else if (PARAM_PATTERNS.DYNAMIC.test(seg)) {
|
|
40
|
+
const name = seg.match(PARAM_PATTERNS.DYNAMIC)[1]
|
|
41
|
+
this.paramNames.push(name)
|
|
42
|
+
regexStr += '/([^/]+)'
|
|
43
|
+
} else if (PARAM_PATTERNS.GROUP.test(seg)) {
|
|
44
|
+
continue
|
|
45
|
+
} else {
|
|
46
|
+
regexStr += `/${seg}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
regexStr += '/?$'
|
|
51
|
+
this.regex = new RegExp(regexStr)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get score() {
|
|
55
|
+
let s = 0
|
|
56
|
+
for (const seg of this.segments) {
|
|
57
|
+
if (PARAM_PATTERNS.OPTIONAL_CATCH_ALL.test(seg)) {
|
|
58
|
+
s += PATTERN_SCORES[SEGMENT_TYPES.OPTIONAL_CATCH_ALL]
|
|
59
|
+
} else if (PARAM_PATTERNS.CATCH_ALL.test(seg)) {
|
|
60
|
+
s += PATTERN_SCORES[SEGMENT_TYPES.CATCH_ALL]
|
|
61
|
+
} else if (PARAM_PATTERNS.DYNAMIC.test(seg)) {
|
|
62
|
+
s += PATTERN_SCORES[SEGMENT_TYPES.DYNAMIC]
|
|
63
|
+
} else if (PARAM_PATTERNS.GROUP.test(seg)) {
|
|
64
|
+
continue
|
|
65
|
+
} else {
|
|
66
|
+
s += PATTERN_SCORES[SEGMENT_TYPES.STATIC]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return s
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe() {
|
|
73
|
+
return {
|
|
74
|
+
pattern: this.raw,
|
|
75
|
+
path: this.path,
|
|
76
|
+
segments: this.segments,
|
|
77
|
+
paramNames: this.paramNames,
|
|
78
|
+
score: this.score,
|
|
79
|
+
regex: this.regex.source,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
match(url) {
|
|
84
|
+
const p = normalizePath(url)
|
|
85
|
+
const m = p.match(this.regex)
|
|
86
|
+
if (!m) return null
|
|
87
|
+
|
|
88
|
+
const params = {}
|
|
89
|
+
let idx = 0
|
|
90
|
+
for (const name of this.paramNames) {
|
|
91
|
+
const val = m[++idx]
|
|
92
|
+
if (val !== undefined) {
|
|
93
|
+
params[name] = val.includes('/') ? val.split('/') : val
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return params
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
stringify(params = {}) {
|
|
100
|
+
let path = this.raw
|
|
101
|
+
for (const [key, val] of Object.entries(params)) {
|
|
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)
|
|
106
|
+
}
|
|
107
|
+
return normalizePath(path)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class RouteMatcher {
|
|
112
|
+
constructor() {
|
|
113
|
+
this._patterns = []
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static create(patterns) {
|
|
117
|
+
const matcher = new RouteMatcher()
|
|
118
|
+
if (patterns) matcher.addMultiple(patterns)
|
|
119
|
+
return matcher
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
add(pattern) {
|
|
123
|
+
const rp = new RoutePattern(pattern)
|
|
124
|
+
this._patterns.push(rp)
|
|
125
|
+
this._patterns.sort((a, b) => b.score - a.score)
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
addMultiple(patterns) {
|
|
130
|
+
for (const p of patterns) this.add(p)
|
|
131
|
+
return this
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
match(url) {
|
|
135
|
+
for (const p of this._patterns) {
|
|
136
|
+
const params = p.match(url)
|
|
137
|
+
if (params !== null) {
|
|
138
|
+
return { pattern: p.raw, path: p.path, params }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
build(pattern, params) {
|
|
145
|
+
return new RoutePattern(pattern).stringify(params)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
clear() {
|
|
149
|
+
this._patterns = []
|
|
150
|
+
return this
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get patterns() {
|
|
154
|
+
return this._patterns.map(p => p.raw)
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/RouteTree.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { normalizePath, isRouteGroup } from './utils.js'
|
|
2
|
+
|
|
3
|
+
class RouteNode {
|
|
4
|
+
constructor({ segment, fullPath, meta } = {}) {
|
|
5
|
+
this.segment = segment || ''
|
|
6
|
+
this.fullPath = fullPath || ''
|
|
7
|
+
this.meta = meta || null
|
|
8
|
+
this.page = null
|
|
9
|
+
this.layout = null
|
|
10
|
+
this.loading = null
|
|
11
|
+
this.error = null
|
|
12
|
+
this.notFound = null
|
|
13
|
+
this.children = []
|
|
14
|
+
this.parent = null
|
|
15
|
+
this.isGroup = isRouteGroup(segment)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addChild(node) {
|
|
19
|
+
node.parent = this
|
|
20
|
+
this.children.push(node)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getLayoutChain() {
|
|
24
|
+
const chain = []
|
|
25
|
+
if (this.layout) chain.push(this.layout)
|
|
26
|
+
let cur = this.parent
|
|
27
|
+
while (cur) {
|
|
28
|
+
if (cur.layout) chain.unshift(cur.layout)
|
|
29
|
+
cur = cur.parent
|
|
30
|
+
}
|
|
31
|
+
return chain
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_findChild(segment) {
|
|
35
|
+
for (const c of this.children) {
|
|
36
|
+
if (c.isGroup) {
|
|
37
|
+
const nested = c._findChild(segment)
|
|
38
|
+
if (nested) return nested
|
|
39
|
+
}
|
|
40
|
+
if (c.segment === segment) return c
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_findDynamic(segment) {
|
|
46
|
+
for (const c of this.children) {
|
|
47
|
+
if (c.isGroup) {
|
|
48
|
+
const nested = c._findDynamic(segment)
|
|
49
|
+
if (nested) return nested
|
|
50
|
+
}
|
|
51
|
+
if (c.segment.startsWith('[') && c.segment.endsWith(']')) return c
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_findCatchAll() {
|
|
57
|
+
for (const c of this.children) {
|
|
58
|
+
if (c.isGroup) {
|
|
59
|
+
const nested = c._findCatchAll()
|
|
60
|
+
if (nested) return nested
|
|
61
|
+
}
|
|
62
|
+
if (c.segment.startsWith('[...') || c.segment.startsWith('[[...')) return c
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class RouteTree {
|
|
69
|
+
constructor(routes) {
|
|
70
|
+
this.root = new RouteNode({ fullPath: '/' })
|
|
71
|
+
this._build(this.root, routes, '/')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static create(routes) {
|
|
75
|
+
return new RouteTree(routes)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_build(parent, routes, base) {
|
|
79
|
+
if (!routes || typeof routes !== 'object') return
|
|
80
|
+
|
|
81
|
+
for (const [key, val] of Object.entries(routes)) {
|
|
82
|
+
const isGroup = isRouteGroup(key)
|
|
83
|
+
const fp = isGroup ? normalizePath(base) : normalizePath(`${base}/${key}`)
|
|
84
|
+
const isRoot = fp === '/' || key === '/'
|
|
85
|
+
|
|
86
|
+
if (isRoot) {
|
|
87
|
+
if (typeof val === 'object' && val !== null) {
|
|
88
|
+
if (val.meta) parent.meta = val.meta
|
|
89
|
+
if (val.layout) parent.layout = val.layout
|
|
90
|
+
if (val.page) parent.page = val.page
|
|
91
|
+
if (val.loading) parent.loading = val.loading
|
|
92
|
+
if (val.error) parent.error = val.error
|
|
93
|
+
if (val.notFound) parent.notFound = val.notFound
|
|
94
|
+
if (val.children) this._build(parent, val.children, fp)
|
|
95
|
+
} else if (typeof val === 'function') {
|
|
96
|
+
parent.page = val
|
|
97
|
+
}
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const seg = isGroup ? key : key.replace(/^\//, '')
|
|
102
|
+
const node = new RouteNode({ segment: seg, fullPath: fp })
|
|
103
|
+
|
|
104
|
+
if (typeof val === 'object' && val !== null) {
|
|
105
|
+
if (val.meta) node.meta = val.meta
|
|
106
|
+
if (val.layout) node.layout = val.layout
|
|
107
|
+
if (val.page) node.page = val.page
|
|
108
|
+
if (val.loading) node.loading = val.loading
|
|
109
|
+
if (val.error) node.error = val.error
|
|
110
|
+
if (val.notFound) node.notFound = val.notFound
|
|
111
|
+
if (val.children) this._build(node, val.children, fp)
|
|
112
|
+
} else if (typeof val === 'function') {
|
|
113
|
+
node.page = val
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
parent.addChild(node)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolve(pathname) {
|
|
121
|
+
const segs = normalizePath(pathname).split('/').filter(Boolean)
|
|
122
|
+
return this._resolve(this.root, segs, 0, {})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_resolve(node, segs, idx, params) {
|
|
126
|
+
if (idx >= segs.length) {
|
|
127
|
+
if (node.page) {
|
|
128
|
+
return { node, params, layouts: node.getLayoutChain() }
|
|
129
|
+
}
|
|
130
|
+
const ca = node._findCatchAll()
|
|
131
|
+
if (ca && ca.page) {
|
|
132
|
+
return { node: ca, params: { ...params }, layouts: ca.getLayoutChain() }
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const seg = segs[idx]
|
|
138
|
+
|
|
139
|
+
const exact = node._findChild(seg)
|
|
140
|
+
if (exact) {
|
|
141
|
+
const res = this._resolve(exact, segs, idx + 1, { ...params })
|
|
142
|
+
if (res) return res
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const dyn = node._findDynamic(seg)
|
|
146
|
+
if (dyn) {
|
|
147
|
+
const name = dyn.segment.replace(/^\[|\]$/g, '')
|
|
148
|
+
const res = this._resolve(dyn, segs, idx + 1, {
|
|
149
|
+
...params,
|
|
150
|
+
[name]: seg,
|
|
151
|
+
})
|
|
152
|
+
if (res) return res
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ca = node._findCatchAll()
|
|
156
|
+
if (ca) {
|
|
157
|
+
const name = ca.segment.replace(/^\[\.\.\.|\]\]?$/g, '')
|
|
158
|
+
return {
|
|
159
|
+
node: ca,
|
|
160
|
+
params: { ...params, [name]: segs.slice(idx) },
|
|
161
|
+
layouts: ca.getLayoutChain(),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
flatten() {
|
|
169
|
+
const result = []
|
|
170
|
+
const walk = (node) => {
|
|
171
|
+
if (node.page) {
|
|
172
|
+
result.push({ path: node.fullPath, page: node.page, meta: node.meta })
|
|
173
|
+
}
|
|
174
|
+
for (const child of node.children) walk(child)
|
|
175
|
+
}
|
|
176
|
+
walk(this.root)
|
|
177
|
+
return result
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
find(pathname) {
|
|
181
|
+
return this.resolve(pathname)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getRoute(pathname) {
|
|
185
|
+
return this.resolve(pathname)
|
|
186
|
+
}
|
|
187
|
+
}
|