metaowl 0.4.0 → 0.5.0
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/CHANGELOG.md +52 -0
- package/README.md +13 -15
- package/build/runtime/bin/metaowl-build.js +10 -0
- package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
- package/build/runtime/bin/metaowl-dev.js +10 -0
- package/build/runtime/bin/metaowl-generate.js +231 -0
- package/build/runtime/bin/metaowl-lint.js +58 -0
- package/build/runtime/bin/utils.js +68 -0
- package/build/runtime/index.js +141 -0
- package/build/runtime/modules/app-mounter.js +65 -0
- package/build/runtime/modules/auto-import.js +140 -0
- package/build/runtime/modules/cache.js +49 -0
- package/build/runtime/modules/composables.js +353 -0
- package/build/runtime/modules/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -0
- package/build/runtime/modules/templates-manager.js +52 -0
- package/build/runtime/modules/test-utils.js +238 -0
- package/build/runtime/vite/plugin.js +183 -0
- package/eslint.js +29 -0
- package/package.json +29 -11
- package/CONTRIBUTING.md +0 -49
- package/bin/metaowl-build.js +0 -12
- package/bin/metaowl-dev.js +0 -12
- package/bin/metaowl-generate.js +0 -339
- package/bin/metaowl-lint.js +0 -71
- package/bin/utils.js +0 -82
- package/index.js +0 -328
- package/modules/app-mounter.js +0 -104
- package/modules/auto-import.js +0 -225
- package/modules/cache.js +0 -59
- package/modules/composables.js +0 -600
- package/modules/error-boundary.js +0 -228
- package/modules/fetch.js +0 -51
- package/modules/file-router.js +0 -478
- package/modules/forms.js +0 -353
- package/modules/i18n.js +0 -333
- package/modules/layouts.js +0 -431
- package/modules/link.js +0 -255
- package/modules/meta.js +0 -119
- package/modules/odoo-rpc.js +0 -511
- package/modules/pwa.js +0 -515
- package/modules/router.js +0 -769
- package/modules/seo.js +0 -501
- package/modules/store.js +0 -409
- package/modules/templates-manager.js +0 -89
- package/modules/test-utils.js +0 -532
- package/test/auto-import.test.js +0 -110
- package/test/cache.test.js +0 -55
- package/test/composables.test.js +0 -103
- package/test/dynamic-routes.test.js +0 -469
- package/test/error-boundary.test.js +0 -126
- package/test/fetch.test.js +0 -100
- package/test/file-router.test.js +0 -55
- package/test/forms.test.js +0 -203
- package/test/i18n.test.js +0 -188
- package/test/layouts.test.js +0 -395
- package/test/link.test.js +0 -189
- package/test/meta.test.js +0 -146
- package/test/odoo-rpc.test.js +0 -547
- package/test/pwa.test.js +0 -154
- package/test/router-guards.test.js +0 -229
- package/test/router.test.js +0 -77
- package/test/seo.test.js +0 -353
- package/test/store.test.js +0 -476
- package/test/templates-manager.test.js +0 -83
- package/test/test-utils.test.js +0 -314
- package/vite/plugin.js +0 -277
- package/vitest.config.js +0 -8
package/modules/router.js
DELETED
|
@@ -1,769 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module Router
|
|
3
|
-
*
|
|
4
|
-
* Enhanced router with navigation guards support.
|
|
5
|
-
*
|
|
6
|
-
* Navigation guards allow intercepting navigation and controlling access
|
|
7
|
-
* to routes based on conditions like authentication, permissions, etc.
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Global beforeEach guards
|
|
11
|
-
* - Global afterEach hooks
|
|
12
|
-
* - Per-route beforeEnter guards
|
|
13
|
-
* - Route metadata support
|
|
14
|
-
* - Navigation cancellation and redirection
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* import { router } from 'metaowl'
|
|
18
|
-
*
|
|
19
|
-
* // Global guard - runs before every navigation
|
|
20
|
-
* router.beforeEach((to, from, next) => {
|
|
21
|
-
* const auth = useAuthStore()
|
|
22
|
-
* if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
|
23
|
-
* next('/login')
|
|
24
|
-
* } else {
|
|
25
|
-
* next()
|
|
26
|
-
* }
|
|
27
|
-
* })
|
|
28
|
-
*
|
|
29
|
-
* // Global after hook - runs after navigation
|
|
30
|
-
* router.afterEach((to, from) => {
|
|
31
|
-
* console.log(`Navigated from ${from.path} to ${to.path}`)
|
|
32
|
-
* })
|
|
33
|
-
*
|
|
34
|
-
* // Per-route guard
|
|
35
|
-
* export class AdminPage extends Component {
|
|
36
|
-
* static route = {
|
|
37
|
-
* path: '/admin',
|
|
38
|
-
* meta: { requiresAuth: true, role: 'admin' },
|
|
39
|
-
* beforeEnter: (to, from, next) => { ... }
|
|
40
|
-
* }
|
|
41
|
-
* }
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
import { Component } from '@odoo/owl'
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Navigation guard function signature:
|
|
48
|
-
* @typedef {Function} NavigationGuard
|
|
49
|
-
* @param {Route} to - Target route
|
|
50
|
-
* @param {Route} from - Current route (or null on initial)
|
|
51
|
-
* @param {Function} next - Callback to resolve navigation
|
|
52
|
-
* - next() - proceed to next guard
|
|
53
|
-
* - next(false) - abort navigation
|
|
54
|
-
* - next('/path') - redirect to path
|
|
55
|
-
* - next({ path: '/path', replace: true }) - redirect with options
|
|
56
|
-
* - next(error) - abort with error
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Current route object.
|
|
61
|
-
* @type {Route|null}
|
|
62
|
-
*/
|
|
63
|
-
let _currentRoute = null
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Previous route object.
|
|
67
|
-
* @type {Route|null}
|
|
68
|
-
*/
|
|
69
|
-
let _previousRoute = null
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Global beforeEach guards.
|
|
73
|
-
* @type {NavigationGuard[]}
|
|
74
|
-
*/
|
|
75
|
-
const _beforeEachGuards = []
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Global afterEach hooks.
|
|
79
|
-
* @type {Function[]}
|
|
80
|
-
*/
|
|
81
|
-
const _afterEachHooks = []
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Navigation in progress flag.
|
|
85
|
-
* @type {boolean}
|
|
86
|
-
*/
|
|
87
|
-
let _isNavigating = false
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Navigation cancellation token.
|
|
91
|
-
* @type {Function|null}
|
|
92
|
-
*/
|
|
93
|
-
let _cancelNavigation = null
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Router instance with guard support.
|
|
97
|
-
*/
|
|
98
|
-
class Router {
|
|
99
|
-
constructor(routes) {
|
|
100
|
-
this.routes = routes
|
|
101
|
-
this.routeMap = new Map()
|
|
102
|
-
|
|
103
|
-
// Build route map for quick lookup
|
|
104
|
-
for (const route of routes) {
|
|
105
|
-
for (const path of route.path) {
|
|
106
|
-
this.routeMap.set(path, route)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Resolve current URL against route table.
|
|
113
|
-
*
|
|
114
|
-
* @param {string} [path] - Optional path to resolve (for testing)
|
|
115
|
-
* @returns {Route|null}
|
|
116
|
-
*/
|
|
117
|
-
resolve(path) {
|
|
118
|
-
const currentPath = path || document.location.pathname
|
|
119
|
-
|
|
120
|
-
// Try exact match first
|
|
121
|
-
if (this.routeMap.has(currentPath)) {
|
|
122
|
-
return this.routeMap.get(currentPath)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Try matching dynamic routes
|
|
126
|
-
for (const route of this.routes) {
|
|
127
|
-
if (this.matchRoute(route, currentPath)) {
|
|
128
|
-
return route
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Match a route against a path.
|
|
137
|
-
*
|
|
138
|
-
* @param {object} route - Route definition
|
|
139
|
-
* @param {string} path - Current path
|
|
140
|
-
* @returns {boolean}
|
|
141
|
-
*/
|
|
142
|
-
matchRoute(route, path) {
|
|
143
|
-
for (const routePath of route.path) {
|
|
144
|
-
if (this.pathMatches(routePath, path)) {
|
|
145
|
-
return true
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Check if a route path matches the current path.
|
|
153
|
-
*
|
|
154
|
-
* Supports:
|
|
155
|
-
* - Exact matches: /about
|
|
156
|
-
* - Dynamic segments: /user/:id
|
|
157
|
-
* - Optional segments: /user/:id?
|
|
158
|
-
* - Wildcards: /user/*
|
|
159
|
-
*
|
|
160
|
-
* @param {string} routePath - Route path pattern
|
|
161
|
-
* @param {string} currentPath - Current URL path
|
|
162
|
-
* @returns {boolean}
|
|
163
|
-
*/
|
|
164
|
-
pathMatches(routePath, currentPath) {
|
|
165
|
-
// Simple exact match for static routes
|
|
166
|
-
if (!routePath.includes(':') && !routePath.includes('*')) {
|
|
167
|
-
const normalizedRoute = routePath.replace(/\/$/, '') || '/'
|
|
168
|
-
const normalizedCurrent = currentPath.replace(/\/$/, '') || '/'
|
|
169
|
-
return normalizedRoute === normalizedCurrent
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Build regex without pre-escaping the whole string (which would break : and * handling)
|
|
173
|
-
let pattern = routePath
|
|
174
|
-
// Escape forward slashes
|
|
175
|
-
.replace(/\//g, '\\/')
|
|
176
|
-
// Replace catch-all :name(.*) params - must come before required-param replacement
|
|
177
|
-
.replace(/:([^/(]+)\(\.\*\)/g, '(.*)')
|
|
178
|
-
// Replace optional params /:name?
|
|
179
|
-
.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?')
|
|
180
|
-
// Replace required params :name
|
|
181
|
-
.replace(/:([^/(?\s]+)/g, '([^/]+)')
|
|
182
|
-
// Replace bare wildcards *
|
|
183
|
-
.replace(/\*/g, '(.*)')
|
|
184
|
-
|
|
185
|
-
pattern = '^' + pattern + '$'
|
|
186
|
-
const regex = new RegExp(pattern)
|
|
187
|
-
|
|
188
|
-
return regex.test(currentPath)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Extract parameters from a matched route.
|
|
193
|
-
*
|
|
194
|
-
* @param {object} route - Route definition
|
|
195
|
-
* @param {string} path - Current path
|
|
196
|
-
* @returns {object} Parameter key-value pairs
|
|
197
|
-
*/
|
|
198
|
-
extractParams(route, path) {
|
|
199
|
-
const params = {}
|
|
200
|
-
|
|
201
|
-
for (const routePath of route.path) {
|
|
202
|
-
const match = this.matchAndExtract(routePath, path)
|
|
203
|
-
if (match) {
|
|
204
|
-
Object.assign(params, match)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return params
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Match a path and extract parameters.
|
|
213
|
-
*
|
|
214
|
-
* @param {string} routePath - Route pattern
|
|
215
|
-
* @param {string} currentPath - Current URL
|
|
216
|
-
* @returns {object|null}
|
|
217
|
-
*/
|
|
218
|
-
matchAndExtract(routePath, currentPath) {
|
|
219
|
-
if (!routePath.includes(':')) {
|
|
220
|
-
return null
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Extract parameter names in the correct order
|
|
224
|
-
const paramNames = []
|
|
225
|
-
|
|
226
|
-
// Build pattern: handle catch-all :name(.*) first, then optional, then required
|
|
227
|
-
let pattern = routePath
|
|
228
|
-
.replace(/:([^/(]+)\(\.\*\)/g, (match, name) => {
|
|
229
|
-
paramNames.push(name)
|
|
230
|
-
return '(.*)'
|
|
231
|
-
})
|
|
232
|
-
.replace(/\/:([^/(]+)\?/g, (match, name) => {
|
|
233
|
-
paramNames.push(name)
|
|
234
|
-
return '(?:/([^/]+))?'
|
|
235
|
-
})
|
|
236
|
-
.replace(/:([^/(?\s]+)/g, (match, name) => {
|
|
237
|
-
paramNames.push(name)
|
|
238
|
-
return '([^/]+)'
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
const regex = new RegExp('^' + pattern + '$')
|
|
242
|
-
const matches = currentPath.match(regex)
|
|
243
|
-
|
|
244
|
-
if (!matches) return null
|
|
245
|
-
|
|
246
|
-
const params = {}
|
|
247
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
248
|
-
if (matches[i + 1] !== undefined) {
|
|
249
|
-
params[paramNames[i]] = matches[i + 1]
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return params
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Route object passed to guards.
|
|
259
|
-
* @typedef {object} Route
|
|
260
|
-
* @property {string} name - Route name
|
|
261
|
-
* @property {string[]} path - URL paths
|
|
262
|
-
* @property {Function} component - Component class
|
|
263
|
-
* @property {object} [meta] - Route metadata
|
|
264
|
-
* @property {NavigationGuard} [beforeEnter] - Per-route guard
|
|
265
|
-
* @property {object} [params] - Dynamic parameters
|
|
266
|
-
*/
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Process routes with guards.
|
|
270
|
-
*
|
|
271
|
-
* @param {object[]} routes - Route table
|
|
272
|
-
* @param {string} [customPath] - Optional custom path for testing
|
|
273
|
-
* @returns {Promise<object[]>} Resolved route or throws error
|
|
274
|
-
* @throws {NavigationError} If navigation is aborted
|
|
275
|
-
*/
|
|
276
|
-
// Tracks which routes arrays have already been augmented with SSG path variants.
|
|
277
|
-
// Using a WeakSet means each distinct array is injected exactly once, even
|
|
278
|
-
// across multiple processRoutes calls with the same array.
|
|
279
|
-
const _injectedRouteSets = new WeakSet()
|
|
280
|
-
|
|
281
|
-
export async function processRoutes(routes, customPath) {
|
|
282
|
-
// Use custom path for testing if provided
|
|
283
|
-
const targetPath = customPath || document.location.pathname
|
|
284
|
-
|
|
285
|
-
// Inject SSG-compatible path variants ONCE per routes array.
|
|
286
|
-
// injectSystemRoutes mutates route.path in-place. Calling it on every
|
|
287
|
-
// navigation causes the arrays to grow on every call (each injected path
|
|
288
|
-
// becomes a base for further injections), making route matching
|
|
289
|
-
// exponentially slower with every navigation.
|
|
290
|
-
if (!_injectedRouteSets.has(routes)) {
|
|
291
|
-
_injectedRouteSets.add(routes)
|
|
292
|
-
for (const route of routes) {
|
|
293
|
-
const originalPaths = [...route.path]
|
|
294
|
-
for (const path of originalPaths) {
|
|
295
|
-
if (typeof path === 'string') {
|
|
296
|
-
injectSystemRoutes(route, path)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const router = new Router(routes)
|
|
303
|
-
const toRoute = router.resolve(targetPath)
|
|
304
|
-
|
|
305
|
-
if (!toRoute) {
|
|
306
|
-
throw new Error(`No route found for "${targetPath}".`)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Build route object
|
|
310
|
-
const to = buildRouteObject(toRoute, router)
|
|
311
|
-
const from = _currentRoute
|
|
312
|
-
|
|
313
|
-
// Run navigation guards
|
|
314
|
-
try {
|
|
315
|
-
await runGuards(to, from, router)
|
|
316
|
-
|
|
317
|
-
// Update current route
|
|
318
|
-
_previousRoute = _currentRoute
|
|
319
|
-
_currentRoute = to
|
|
320
|
-
|
|
321
|
-
// Run afterEach hooks
|
|
322
|
-
for (const hook of _afterEachHooks) {
|
|
323
|
-
hook(to, from)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return [toRoute]
|
|
327
|
-
} catch (error) {
|
|
328
|
-
if (error.name === 'NavigationRedirect') {
|
|
329
|
-
// Redirect to new location
|
|
330
|
-
if (error.path) {
|
|
331
|
-
window.location.href = error.path
|
|
332
|
-
return null
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
throw error
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Build a route object for guards.
|
|
341
|
-
*
|
|
342
|
-
* @param {object} routeDef - Raw route definition
|
|
343
|
-
* @param {Router} router - Router instance
|
|
344
|
-
* @returns {Route}
|
|
345
|
-
*/
|
|
346
|
-
function buildRouteObject(routeDef, router) {
|
|
347
|
-
const currentPath = document.location.pathname
|
|
348
|
-
const params = router.extractParams(routeDef, currentPath)
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
name: routeDef.name,
|
|
352
|
-
path: routeDef.path,
|
|
353
|
-
fullPath: currentPath,
|
|
354
|
-
component: routeDef.component,
|
|
355
|
-
meta: routeDef.meta || {},
|
|
356
|
-
beforeEnter: routeDef.beforeEnter,
|
|
357
|
-
params,
|
|
358
|
-
query: parseQuery(document.location.search)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Parse query string into object.
|
|
364
|
-
*
|
|
365
|
-
* @param {string} search - Query string (e.g., '?foo=bar&baz=qux')
|
|
366
|
-
* @returns {object}
|
|
367
|
-
*/
|
|
368
|
-
function parseQuery(search) {
|
|
369
|
-
const query = {}
|
|
370
|
-
if (!search || search === '?') return query
|
|
371
|
-
|
|
372
|
-
const params = new URLSearchParams(search.substring(1))
|
|
373
|
-
for (const [key, value] of params) {
|
|
374
|
-
if (query[key]) {
|
|
375
|
-
if (Array.isArray(query[key])) {
|
|
376
|
-
query[key].push(value)
|
|
377
|
-
} else {
|
|
378
|
-
query[key] = [query[key], value]
|
|
379
|
-
}
|
|
380
|
-
} else {
|
|
381
|
-
query[key] = value
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return query
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Run all navigation guards.
|
|
390
|
-
*
|
|
391
|
-
* @param {Route} to - Target route
|
|
392
|
-
* @param {Route} from - Current route
|
|
393
|
-
* @param {Router} router - Router instance
|
|
394
|
-
*/
|
|
395
|
-
async function runGuards(to, from, router) {
|
|
396
|
-
_isNavigating = true
|
|
397
|
-
|
|
398
|
-
// Create navigation controller
|
|
399
|
-
let cancelled = false
|
|
400
|
-
_cancelNavigation = () => { cancelled = true }
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
// Run global beforeEach guards
|
|
404
|
-
for (const guard of _beforeEachGuards) {
|
|
405
|
-
if (cancelled) break
|
|
406
|
-
|
|
407
|
-
const result = await runGuard(guard, to, from)
|
|
408
|
-
|
|
409
|
-
if (result === false) {
|
|
410
|
-
throw new NavigationCancelled()
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (typeof result === 'string') {
|
|
414
|
-
throw new NavigationRedirect(result)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (result && typeof result === 'object' && result.path) {
|
|
418
|
-
throw new NavigationRedirect(result.path)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Run per-route beforeEnter
|
|
423
|
-
if (to.beforeEnter && !cancelled) {
|
|
424
|
-
const result = await runGuard(to.beforeEnter, to, from)
|
|
425
|
-
|
|
426
|
-
if (result === false) {
|
|
427
|
-
throw new NavigationCancelled()
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (typeof result === 'string') {
|
|
431
|
-
throw new NavigationRedirect(result)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
} finally {
|
|
435
|
-
_isNavigating = false
|
|
436
|
-
_cancelNavigation = null
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Run a single guard with next callback support.
|
|
442
|
-
*
|
|
443
|
-
* @param {Function} guard - Guard function
|
|
444
|
-
* @param {Route} to - Target route
|
|
445
|
-
* @param {Route} from - Current route
|
|
446
|
-
* @returns {Promise<*>}
|
|
447
|
-
*/
|
|
448
|
-
async function runGuard(guard, to, from) {
|
|
449
|
-
return new Promise((resolve, reject) => {
|
|
450
|
-
const next = (result) => {
|
|
451
|
-
if (result instanceof Error) {
|
|
452
|
-
reject(result)
|
|
453
|
-
} else {
|
|
454
|
-
resolve(result)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
const guardResult = guard(to, from, next)
|
|
460
|
-
|
|
461
|
-
// Handle both sync and async guards
|
|
462
|
-
if (guardResult && typeof guardResult.then === 'function') {
|
|
463
|
-
guardResult.then(resolve).catch(reject)
|
|
464
|
-
} else if (guardResult !== undefined) {
|
|
465
|
-
// Sync guard returned a value
|
|
466
|
-
resolve(guardResult)
|
|
467
|
-
}
|
|
468
|
-
// If undefined, guard called next() callback
|
|
469
|
-
} catch (error) {
|
|
470
|
-
reject(error)
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Reset router state (for testing purposes).
|
|
477
|
-
*/
|
|
478
|
-
export function resetRouter() {
|
|
479
|
-
_beforeEachGuards.length = 0
|
|
480
|
-
_afterEachHooks.length = 0
|
|
481
|
-
_isNavigating = false
|
|
482
|
-
_cancelNavigation = null
|
|
483
|
-
_currentRoute = null
|
|
484
|
-
_previousRoute = null
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Navigation cancelled error.
|
|
489
|
-
*/
|
|
490
|
-
class NavigationCancelled extends Error {
|
|
491
|
-
constructor() {
|
|
492
|
-
super('Navigation cancelled')
|
|
493
|
-
this.name = 'NavigationCancelled'
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Navigation redirect error.
|
|
499
|
-
*/
|
|
500
|
-
class NavigationRedirect extends Error {
|
|
501
|
-
constructor(path) {
|
|
502
|
-
super('Navigation redirect')
|
|
503
|
-
this.name = 'NavigationRedirect'
|
|
504
|
-
this.path = path
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// --- Public Router API ---
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Register a global guard to be called before every navigation.
|
|
512
|
-
*
|
|
513
|
-
* @param {NavigationGuard} guard - Guard function
|
|
514
|
-
* @returns {Function} Remove guard function
|
|
515
|
-
*/
|
|
516
|
-
export function beforeEach(guard) {
|
|
517
|
-
_beforeEachGuards.push(guard)
|
|
518
|
-
return () => {
|
|
519
|
-
const index = _beforeEachGuards.indexOf(guard)
|
|
520
|
-
if (index > -1) _beforeEachGuards.splice(index, 1)
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Register a hook to be called after every navigation.
|
|
526
|
-
*
|
|
527
|
-
* @param {Function} hook - Hook function (to, from) => void
|
|
528
|
-
* @returns {Function} Remove hook function
|
|
529
|
-
*/
|
|
530
|
-
export function afterEach(hook) {
|
|
531
|
-
_afterEachHooks.push(hook)
|
|
532
|
-
return () => {
|
|
533
|
-
const index = _afterEachHooks.indexOf(hook)
|
|
534
|
-
if (index > -1) _afterEachHooks.splice(index, 1)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Get the current route.
|
|
540
|
-
*
|
|
541
|
-
* @returns {Route|null}
|
|
542
|
-
*/
|
|
543
|
-
export function getCurrentRoute() {
|
|
544
|
-
return _currentRoute
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Get the previous route.
|
|
549
|
-
*
|
|
550
|
-
* @returns {Route|null}
|
|
551
|
-
*/
|
|
552
|
-
export function getPreviousRoute() {
|
|
553
|
-
return _previousRoute
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Check if navigation is in progress.
|
|
558
|
-
*
|
|
559
|
-
* @returns {boolean}
|
|
560
|
-
*/
|
|
561
|
-
export function isNavigating() {
|
|
562
|
-
return _isNavigating
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Cancel current navigation (if in progress).
|
|
567
|
-
*/
|
|
568
|
-
export function cancelNavigation() {
|
|
569
|
-
if (_cancelNavigation) {
|
|
570
|
-
_cancelNavigation()
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Callback for SPA navigation.
|
|
576
|
-
* Set when the app is initialized.
|
|
577
|
-
* @type {Function|null}
|
|
578
|
-
*/
|
|
579
|
-
let _spaNavigationCallback = null
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Sets the SPA navigation callback.
|
|
583
|
-
* Called internally by boot().
|
|
584
|
-
*
|
|
585
|
-
* @param {Function} callback - Function called during SPA navigation
|
|
586
|
-
* @internal
|
|
587
|
-
*/
|
|
588
|
-
export function _setSpaNavigationCallback(callback) {
|
|
589
|
-
_spaNavigationCallback = callback
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Flag indicating if SPA navigation is enabled.
|
|
594
|
-
* @type {boolean}
|
|
595
|
-
*/
|
|
596
|
-
let _spaEnabled = false
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Enables or disables SPA navigation.
|
|
600
|
-
*
|
|
601
|
-
* @param {boolean} enabled - True to enable SPA navigation
|
|
602
|
-
*/
|
|
603
|
-
export function setSpaMode(enabled) {
|
|
604
|
-
_spaEnabled = enabled
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Checks if SPA navigation is enabled.
|
|
609
|
-
*
|
|
610
|
-
* @returns {boolean}
|
|
611
|
-
*/
|
|
612
|
-
export function isSpaMode() {
|
|
613
|
-
return _spaEnabled
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Navigate to a path with SPA navigation (no page reload).
|
|
618
|
-
* Updates URL via history.pushState and renders the new route.
|
|
619
|
-
*
|
|
620
|
-
* @param {string} path - Target path (e.g., "/about")
|
|
621
|
-
* @param {object} [options] - Navigation options
|
|
622
|
-
* @param {boolean} [options.replace=false] - Replace current history entry instead of creating new one
|
|
623
|
-
* @returns {Promise<boolean>} True if navigation successful
|
|
624
|
-
*
|
|
625
|
-
* @example
|
|
626
|
-
* // Normal navigation
|
|
627
|
-
* await navigateTo('/about')
|
|
628
|
-
*
|
|
629
|
-
* // Replace current entry (no back possible)
|
|
630
|
-
* await navigateTo('/login', { replace: true })
|
|
631
|
-
*/
|
|
632
|
-
export async function navigateTo(path, options = {}) {
|
|
633
|
-
const { replace = false } = options
|
|
634
|
-
|
|
635
|
-
if (!_spaEnabled || !_spaNavigationCallback) {
|
|
636
|
-
// Fallback: Normal browser navigation
|
|
637
|
-
if (replace) {
|
|
638
|
-
window.location.replace(path)
|
|
639
|
-
} else {
|
|
640
|
-
window.location.href = path
|
|
641
|
-
}
|
|
642
|
-
return false
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
// Update URL without page reload
|
|
647
|
-
if (replace) {
|
|
648
|
-
window.history.replaceState({ path }, '', path)
|
|
649
|
-
} else {
|
|
650
|
-
window.history.pushState({ path }, '', path)
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Perform SPA navigation
|
|
654
|
-
await _spaNavigationCallback(path)
|
|
655
|
-
return true
|
|
656
|
-
} catch (error) {
|
|
657
|
-
console.error('[metaowl] SPA navigation failed:', error)
|
|
658
|
-
// Fallback to normal navigation on error
|
|
659
|
-
window.location.href = path
|
|
660
|
-
return false
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Programmatically navigate to a path.
|
|
666
|
-
*
|
|
667
|
-
* @param {string} path - Target path
|
|
668
|
-
* @param {object} [options] - Navigation options
|
|
669
|
-
* @param {boolean} [options.replace=false] - Replace current history entry
|
|
670
|
-
* @param {boolean} [options.reload=true] - Reload the page
|
|
671
|
-
* @deprecated Use navigateTo() for SPA navigation
|
|
672
|
-
*/
|
|
673
|
-
export function navigate(path, options = {}) {
|
|
674
|
-
const { replace = false, reload = true } = options
|
|
675
|
-
|
|
676
|
-
if (reload || !_spaEnabled) {
|
|
677
|
-
// Traditional navigation with page reload
|
|
678
|
-
if (replace) {
|
|
679
|
-
window.location.replace(path)
|
|
680
|
-
} else {
|
|
681
|
-
window.location.href = path
|
|
682
|
-
}
|
|
683
|
-
} else {
|
|
684
|
-
// SPA navigation
|
|
685
|
-
navigateTo(path, { replace })
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Push a new history entry.
|
|
691
|
-
*
|
|
692
|
-
* @param {string} path - Target path
|
|
693
|
-
*/
|
|
694
|
-
export function push(path) {
|
|
695
|
-
navigateTo(path, { replace: false })
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Replace current history entry.
|
|
700
|
-
*
|
|
701
|
-
* @param {string} path - Target path
|
|
702
|
-
*/
|
|
703
|
-
export function replace(path) {
|
|
704
|
-
navigateTo(path, { replace: true })
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Go back in history.
|
|
709
|
-
*/
|
|
710
|
-
export function back() {
|
|
711
|
-
window.history.back()
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Go forward in history.
|
|
716
|
-
*/
|
|
717
|
-
export function forward() {
|
|
718
|
-
window.history.forward()
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Go n steps in history.
|
|
723
|
-
*
|
|
724
|
-
* @param {number} n - Steps to go (negative for back)
|
|
725
|
-
*/
|
|
726
|
-
export function go(n) {
|
|
727
|
-
window.history.go(n)
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Router singleton with guard methods.
|
|
732
|
-
*/
|
|
733
|
-
export const router = {
|
|
734
|
-
beforeEach,
|
|
735
|
-
afterEach,
|
|
736
|
-
get currentRoute() { return getCurrentRoute() },
|
|
737
|
-
get previousRoute() { return getPreviousRoute() },
|
|
738
|
-
get isNavigating() { return isNavigating() },
|
|
739
|
-
cancel: cancelNavigation,
|
|
740
|
-
push,
|
|
741
|
-
replace,
|
|
742
|
-
back,
|
|
743
|
-
forward,
|
|
744
|
-
go,
|
|
745
|
-
navigate,
|
|
746
|
-
navigateTo,
|
|
747
|
-
setSpaMode,
|
|
748
|
-
isSpaMode
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Injects SSG-compatible path variants for a route:
|
|
753
|
-
* trailing slash, .html suffix, /index.html suffix.
|
|
754
|
-
*
|
|
755
|
-
* @param {object} route
|
|
756
|
-
* @param {string} path
|
|
757
|
-
* @returns {object}
|
|
758
|
-
*/
|
|
759
|
-
function injectSystemRoutes(route, path) {
|
|
760
|
-
if (path === '/') {
|
|
761
|
-
if (!route.path.includes('/index.html')) route.path.push('/index.html')
|
|
762
|
-
} else {
|
|
763
|
-
if (!route.path.includes(`${path}.html`)) route.path.push(`${path}.html`)
|
|
764
|
-
if (!route.path.includes(`${path}/`)) route.path.push(`${path}/`)
|
|
765
|
-
if (!route.path.includes(`${path}/index.html`)) route.path.push(`${path}/index.html`)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return route
|
|
769
|
-
}
|