metaowl 0.4.1 → 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 +22 -0
- package/README.md +12 -0
- 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 +28 -10
- 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/eslint.config.js +0 -3
- 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 -290
- package/vitest.config.js +0 -8
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module PWA
|
|
3
|
+
*
|
|
4
|
+
* Progressive Web App utilities for MetaOwl applications.
|
|
5
|
+
*/
|
|
6
|
+
export function generateManifest(options) {
|
|
7
|
+
const { name, shortName, description, startUrl = './', display = 'standalone', themeColor = '#000000', backgroundColor = '#ffffff', scope = './', icons = [] } = options;
|
|
8
|
+
const manifest = {
|
|
9
|
+
name,
|
|
10
|
+
short_name: shortName,
|
|
11
|
+
start_url: startUrl,
|
|
12
|
+
display,
|
|
13
|
+
theme_color: themeColor,
|
|
14
|
+
background_color: backgroundColor,
|
|
15
|
+
scope,
|
|
16
|
+
orientation: 'any'
|
|
17
|
+
};
|
|
18
|
+
if (description) {
|
|
19
|
+
manifest.description = description;
|
|
20
|
+
}
|
|
21
|
+
if (icons.length > 0) {
|
|
22
|
+
manifest.icons = icons;
|
|
23
|
+
}
|
|
24
|
+
return manifest;
|
|
25
|
+
}
|
|
26
|
+
export async function registerServiceWorker(path, options = {}) {
|
|
27
|
+
const { onUpdate, onReady } = options;
|
|
28
|
+
if (!('serviceWorker' in navigator)) {
|
|
29
|
+
console.warn('[PWA] Service workers not supported');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const registration = await navigator.serviceWorker.register(path);
|
|
34
|
+
registration.addEventListener('updatefound', () => {
|
|
35
|
+
const newWorker = registration.installing;
|
|
36
|
+
if (!newWorker)
|
|
37
|
+
return;
|
|
38
|
+
newWorker.addEventListener('statechange', () => {
|
|
39
|
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
40
|
+
if (onUpdate) {
|
|
41
|
+
onUpdate(registration);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (newWorker.state === 'activated') {
|
|
45
|
+
if (onReady) {
|
|
46
|
+
onReady(registration);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
if (registration.active && onReady) {
|
|
52
|
+
onReady(registration);
|
|
53
|
+
}
|
|
54
|
+
return registration;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('[PWA] Service worker registration failed:', error);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function unregisterServiceWorker() {
|
|
62
|
+
if (!('serviceWorker' in navigator)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const registration = await navigator.serviceWorker.ready;
|
|
67
|
+
await registration.unregister();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function isStandalone() {
|
|
75
|
+
if (window.matchMedia) {
|
|
76
|
+
const standaloneNavigator = window.navigator;
|
|
77
|
+
return window.matchMedia('(display-mode: standalone)').matches || standaloneNavigator.standalone === true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
export function isOnline() {
|
|
82
|
+
return navigator.onLine;
|
|
83
|
+
}
|
|
84
|
+
export function subscribeToConnectivity(callbacks) {
|
|
85
|
+
const { onOnline, onOffline } = callbacks;
|
|
86
|
+
const handleOnline = () => {
|
|
87
|
+
if (onOnline)
|
|
88
|
+
onOnline();
|
|
89
|
+
};
|
|
90
|
+
const handleOffline = () => {
|
|
91
|
+
if (onOffline)
|
|
92
|
+
onOffline();
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener('online', handleOnline);
|
|
95
|
+
window.addEventListener('offline', handleOffline);
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener('online', handleOnline);
|
|
98
|
+
window.removeEventListener('offline', handleOffline);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export async function requestPersistentStorage() {
|
|
102
|
+
if (navigator.storage?.persist) {
|
|
103
|
+
return await navigator.storage.persist();
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
export async function getStorageInfo() {
|
|
108
|
+
if (navigator.storage?.estimate) {
|
|
109
|
+
return await navigator.storage.estimate();
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
export async function sync(tag) {
|
|
114
|
+
if (!('serviceWorker' in navigator)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const registration = await navigator.serviceWorker.ready;
|
|
119
|
+
const syncRegistration = registration;
|
|
120
|
+
if (syncRegistration.sync) {
|
|
121
|
+
await syncRegistration.sync.register(tag);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Background sync not supported or failed
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
export async function subscribeToPush(options) {
|
|
131
|
+
const { serverUrl, publicKey } = options;
|
|
132
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
133
|
+
console.warn('[PWA] Push notifications not supported');
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const permission = await Notification.requestPermission();
|
|
138
|
+
if (permission !== 'granted') {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const registration = await navigator.serviceWorker.ready;
|
|
142
|
+
const subscription = await registration.pushManager.subscribe({
|
|
143
|
+
userVisibleOnly: true,
|
|
144
|
+
applicationServerKey: urlBase64ToUint8Array(publicKey)
|
|
145
|
+
});
|
|
146
|
+
if (serverUrl) {
|
|
147
|
+
await fetch(serverUrl, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify(subscription)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return subscription;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('[PWA] Push subscription failed:', error);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function unsubscribeFromPush() {
|
|
161
|
+
if (!('serviceWorker' in navigator)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const registration = await navigator.serviceWorker.ready;
|
|
166
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
167
|
+
if (subscription) {
|
|
168
|
+
await subscription.unsubscribe();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Ignore errors
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
export async function showNotification(title, options = {}) {
|
|
178
|
+
if (!('serviceWorker' in navigator)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const registration = await navigator.serviceWorker.ready;
|
|
183
|
+
await registration.showNotification(title, options);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error('[PWA] Show notification failed:', error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function urlBase64ToUint8Array(base64String) {
|
|
190
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
191
|
+
const base64 = (base64String + padding)
|
|
192
|
+
.replace(/-/g, '+')
|
|
193
|
+
.replace(/_/g, '/');
|
|
194
|
+
const rawData = window.atob(base64);
|
|
195
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
196
|
+
for (let index = 0; index < rawData.length; ++index) {
|
|
197
|
+
outputArray[index] = rawData.charCodeAt(index);
|
|
198
|
+
}
|
|
199
|
+
return outputArray;
|
|
200
|
+
}
|
|
201
|
+
export const cache = {
|
|
202
|
+
async add(cacheName, urls) {
|
|
203
|
+
if (!('caches' in window))
|
|
204
|
+
return;
|
|
205
|
+
const cacheStorage = await caches.open(cacheName);
|
|
206
|
+
await cacheStorage.addAll(urls);
|
|
207
|
+
},
|
|
208
|
+
async remove(cacheName, urls) {
|
|
209
|
+
if (!('caches' in window))
|
|
210
|
+
return;
|
|
211
|
+
const cacheStorage = await caches.open(cacheName);
|
|
212
|
+
for (const url of urls) {
|
|
213
|
+
await cacheStorage.delete(url);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async clear() {
|
|
217
|
+
if (!('caches' in window))
|
|
218
|
+
return;
|
|
219
|
+
const cacheKeys = await caches.keys();
|
|
220
|
+
await Promise.all(cacheKeys.map((key) => caches.delete(key)));
|
|
221
|
+
},
|
|
222
|
+
async info() {
|
|
223
|
+
if (!('caches' in window))
|
|
224
|
+
return [];
|
|
225
|
+
const cacheKeys = await caches.keys();
|
|
226
|
+
const info = [];
|
|
227
|
+
for (const key of cacheKeys) {
|
|
228
|
+
const cacheStorage = await caches.open(key);
|
|
229
|
+
const requests = await cacheStorage.keys();
|
|
230
|
+
info.push({ name: key, size: requests.length });
|
|
231
|
+
}
|
|
232
|
+
return info;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
export function checkCapabilities() {
|
|
236
|
+
return {
|
|
237
|
+
serviceWorker: 'serviceWorker' in navigator,
|
|
238
|
+
push: 'PushManager' in window,
|
|
239
|
+
notifications: 'Notification' in window,
|
|
240
|
+
backgroundSync: false,
|
|
241
|
+
persistentStorage: Boolean(navigator.storage?.persist),
|
|
242
|
+
addToHomeScreen: !isStandalone(),
|
|
243
|
+
offline: 'serviceWorker' in navigator
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export const PWA = {
|
|
247
|
+
generateManifest,
|
|
248
|
+
registerServiceWorker,
|
|
249
|
+
unregisterServiceWorker,
|
|
250
|
+
isStandalone,
|
|
251
|
+
isOnline,
|
|
252
|
+
subscribeToConnectivity,
|
|
253
|
+
requestPersistentStorage,
|
|
254
|
+
getStorageInfo,
|
|
255
|
+
sync,
|
|
256
|
+
subscribeToPush,
|
|
257
|
+
unsubscribeFromPush,
|
|
258
|
+
showNotification,
|
|
259
|
+
cache,
|
|
260
|
+
checkCapabilities
|
|
261
|
+
};
|
|
262
|
+
export default PWA;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Router
|
|
3
|
+
*
|
|
4
|
+
* Enhanced router with navigation guards support.
|
|
5
|
+
*/
|
|
6
|
+
let currentRoute = null;
|
|
7
|
+
let previousRoute = null;
|
|
8
|
+
const beforeEachGuards = [];
|
|
9
|
+
const afterEachHooks = [];
|
|
10
|
+
let navigating = false;
|
|
11
|
+
let cancelCurrentNavigation = null;
|
|
12
|
+
let spaNavigationCallback = null;
|
|
13
|
+
let spaEnabled = false;
|
|
14
|
+
class Router {
|
|
15
|
+
routes;
|
|
16
|
+
routeMap;
|
|
17
|
+
constructor(routes) {
|
|
18
|
+
this.routes = routes;
|
|
19
|
+
this.routeMap = new Map();
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
for (const path of route.path) {
|
|
22
|
+
this.routeMap.set(path, route);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
resolve(path) {
|
|
27
|
+
const currentPath = path || document.location.pathname;
|
|
28
|
+
if (this.routeMap.has(currentPath)) {
|
|
29
|
+
return this.routeMap.get(currentPath) || null;
|
|
30
|
+
}
|
|
31
|
+
for (const route of this.routes) {
|
|
32
|
+
if (this.matchRoute(route, currentPath)) {
|
|
33
|
+
return route;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
matchRoute(route, path) {
|
|
39
|
+
for (const routePath of route.path) {
|
|
40
|
+
if (this.pathMatches(routePath, path)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
pathMatches(routePath, currentPath) {
|
|
47
|
+
if (!routePath.includes(':') && !routePath.includes('*')) {
|
|
48
|
+
const normalizedRoute = routePath.replace(/\/$/, '') || '/';
|
|
49
|
+
const normalizedCurrent = currentPath.replace(/\/$/, '') || '/';
|
|
50
|
+
return normalizedRoute === normalizedCurrent;
|
|
51
|
+
}
|
|
52
|
+
let pattern = routePath
|
|
53
|
+
.replace(/\//g, '\\/')
|
|
54
|
+
.replace(/:([^/(]+)\(\.\*\)/g, '(.*)')
|
|
55
|
+
.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?')
|
|
56
|
+
.replace(/:([^/(?\s]+)/g, '([^/]+)')
|
|
57
|
+
.replace(/\*/g, '(.*)');
|
|
58
|
+
pattern = '^' + pattern + '$';
|
|
59
|
+
return new RegExp(pattern).test(currentPath);
|
|
60
|
+
}
|
|
61
|
+
extractParams(route, path) {
|
|
62
|
+
const params = {};
|
|
63
|
+
for (const routePath of route.path) {
|
|
64
|
+
const match = this.matchAndExtract(routePath, path);
|
|
65
|
+
if (match) {
|
|
66
|
+
Object.assign(params, match);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return params;
|
|
70
|
+
}
|
|
71
|
+
matchAndExtract(routePath, currentPath) {
|
|
72
|
+
if (!routePath.includes(':')) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const paramNames = [];
|
|
76
|
+
const pattern = routePath
|
|
77
|
+
.replace(/:([^/(]+)\(\.\*\)/g, (_match, name) => {
|
|
78
|
+
paramNames.push(name);
|
|
79
|
+
return '(.*)';
|
|
80
|
+
})
|
|
81
|
+
.replace(/\/:([^/(]+)\?/g, (_match, name) => {
|
|
82
|
+
paramNames.push(name);
|
|
83
|
+
return '(?:/([^/]+))?';
|
|
84
|
+
})
|
|
85
|
+
.replace(/:([^/(?\s]+)/g, (_match, name) => {
|
|
86
|
+
paramNames.push(name);
|
|
87
|
+
return '([^/]+)';
|
|
88
|
+
});
|
|
89
|
+
const matches = currentPath.match(new RegExp('^' + pattern + '$'));
|
|
90
|
+
if (!matches)
|
|
91
|
+
return null;
|
|
92
|
+
const params = {};
|
|
93
|
+
for (let index = 0; index < paramNames.length; index++) {
|
|
94
|
+
const value = matches[index + 1];
|
|
95
|
+
if (value !== undefined) {
|
|
96
|
+
params[paramNames[index]] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return params;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const injectedRouteSets = new WeakSet();
|
|
103
|
+
export async function processRoutes(routes, customPath) {
|
|
104
|
+
const targetPath = customPath || document.location.pathname;
|
|
105
|
+
if (!injectedRouteSets.has(routes)) {
|
|
106
|
+
injectedRouteSets.add(routes);
|
|
107
|
+
for (const route of routes) {
|
|
108
|
+
const originalPaths = [...route.path];
|
|
109
|
+
for (const path of originalPaths) {
|
|
110
|
+
if (typeof path === 'string') {
|
|
111
|
+
injectSystemRoutes(route, path);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const routerInstance = new Router(routes);
|
|
117
|
+
const toRoute = routerInstance.resolve(targetPath);
|
|
118
|
+
if (!toRoute) {
|
|
119
|
+
throw new Error(`No route found for "${targetPath}".`);
|
|
120
|
+
}
|
|
121
|
+
const to = buildRouteObject(toRoute, routerInstance);
|
|
122
|
+
const from = currentRoute;
|
|
123
|
+
try {
|
|
124
|
+
await runGuards(to, from);
|
|
125
|
+
previousRoute = currentRoute;
|
|
126
|
+
currentRoute = to;
|
|
127
|
+
for (const hook of afterEachHooks) {
|
|
128
|
+
hook(to, from);
|
|
129
|
+
}
|
|
130
|
+
return [toRoute];
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (isNavigationRedirect(error) && error.path) {
|
|
134
|
+
window.location.href = error.path;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function buildRouteObject(routeDef, routerInstance) {
|
|
141
|
+
const currentPath = document.location.pathname;
|
|
142
|
+
const params = routerInstance.extractParams(routeDef, currentPath);
|
|
143
|
+
return {
|
|
144
|
+
name: routeDef.name,
|
|
145
|
+
path: routeDef.path,
|
|
146
|
+
fullPath: currentPath,
|
|
147
|
+
component: routeDef.component,
|
|
148
|
+
meta: routeDef.meta || {},
|
|
149
|
+
beforeEnter: routeDef.beforeEnter,
|
|
150
|
+
params,
|
|
151
|
+
query: parseQuery(document.location.search)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseQuery(search) {
|
|
155
|
+
const query = {};
|
|
156
|
+
if (!search || search === '?')
|
|
157
|
+
return query;
|
|
158
|
+
const params = new URLSearchParams(search.substring(1));
|
|
159
|
+
for (const [key, value] of params) {
|
|
160
|
+
const existing = query[key];
|
|
161
|
+
if (existing) {
|
|
162
|
+
if (Array.isArray(existing)) {
|
|
163
|
+
existing.push(value);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
query[key] = [existing, value];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
query[key] = value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return query;
|
|
174
|
+
}
|
|
175
|
+
async function runGuards(to, from) {
|
|
176
|
+
navigating = true;
|
|
177
|
+
let cancelled = false;
|
|
178
|
+
cancelCurrentNavigation = () => {
|
|
179
|
+
cancelled = true;
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
for (const guard of beforeEachGuards) {
|
|
183
|
+
if (cancelled)
|
|
184
|
+
break;
|
|
185
|
+
const result = await runGuard(guard, to, from);
|
|
186
|
+
if (result === false) {
|
|
187
|
+
throw new NavigationCancelled();
|
|
188
|
+
}
|
|
189
|
+
if (typeof result === 'string') {
|
|
190
|
+
throw new NavigationRedirect(result);
|
|
191
|
+
}
|
|
192
|
+
if (result && typeof result === 'object' && 'path' in result && typeof result.path === 'string') {
|
|
193
|
+
throw new NavigationRedirect(result.path);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (to.beforeEnter && !cancelled) {
|
|
197
|
+
const result = await runGuard(to.beforeEnter, to, from);
|
|
198
|
+
if (result === false) {
|
|
199
|
+
throw new NavigationCancelled();
|
|
200
|
+
}
|
|
201
|
+
if (typeof result === 'string') {
|
|
202
|
+
throw new NavigationRedirect(result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
navigating = false;
|
|
208
|
+
cancelCurrentNavigation = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function runGuard(guard, to, from) {
|
|
212
|
+
return await new Promise((resolve, reject) => {
|
|
213
|
+
const next = (result) => {
|
|
214
|
+
if (result instanceof Error) {
|
|
215
|
+
reject(result);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
resolve(result);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
const guardResult = guard(to, from, next);
|
|
223
|
+
if (guardResult && typeof guardResult.then === 'function') {
|
|
224
|
+
;
|
|
225
|
+
guardResult.then(resolve).catch(reject);
|
|
226
|
+
}
|
|
227
|
+
else if (guardResult !== undefined) {
|
|
228
|
+
resolve(guardResult);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
reject(error);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export function resetRouter() {
|
|
237
|
+
beforeEachGuards.length = 0;
|
|
238
|
+
afterEachHooks.length = 0;
|
|
239
|
+
navigating = false;
|
|
240
|
+
cancelCurrentNavigation = null;
|
|
241
|
+
currentRoute = null;
|
|
242
|
+
previousRoute = null;
|
|
243
|
+
spaNavigationCallback = null;
|
|
244
|
+
spaEnabled = false;
|
|
245
|
+
}
|
|
246
|
+
class NavigationCancelled extends Error {
|
|
247
|
+
constructor() {
|
|
248
|
+
super('Navigation cancelled');
|
|
249
|
+
this.name = 'NavigationCancelled';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
class NavigationRedirect extends Error {
|
|
253
|
+
path;
|
|
254
|
+
constructor(path) {
|
|
255
|
+
super('Navigation redirect');
|
|
256
|
+
this.name = 'NavigationRedirect';
|
|
257
|
+
this.path = path;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isNavigationRedirect(error) {
|
|
261
|
+
return error instanceof Error && error.name === 'NavigationRedirect' && 'path' in error;
|
|
262
|
+
}
|
|
263
|
+
export function beforeEach(guard) {
|
|
264
|
+
beforeEachGuards.push(guard);
|
|
265
|
+
return () => {
|
|
266
|
+
const index = beforeEachGuards.indexOf(guard);
|
|
267
|
+
if (index > -1)
|
|
268
|
+
beforeEachGuards.splice(index, 1);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
export function afterEach(hook) {
|
|
272
|
+
afterEachHooks.push(hook);
|
|
273
|
+
return () => {
|
|
274
|
+
const index = afterEachHooks.indexOf(hook);
|
|
275
|
+
if (index > -1)
|
|
276
|
+
afterEachHooks.splice(index, 1);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function getCurrentRoute() {
|
|
280
|
+
return currentRoute;
|
|
281
|
+
}
|
|
282
|
+
export function getPreviousRoute() {
|
|
283
|
+
return previousRoute;
|
|
284
|
+
}
|
|
285
|
+
export function isNavigating() {
|
|
286
|
+
return navigating;
|
|
287
|
+
}
|
|
288
|
+
export function cancelNavigation() {
|
|
289
|
+
if (cancelCurrentNavigation) {
|
|
290
|
+
cancelCurrentNavigation();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
export function _setSpaNavigationCallback(callback) {
|
|
294
|
+
spaNavigationCallback = callback;
|
|
295
|
+
}
|
|
296
|
+
export function setSpaMode(enabled) {
|
|
297
|
+
spaEnabled = enabled;
|
|
298
|
+
}
|
|
299
|
+
export function isSpaMode() {
|
|
300
|
+
return spaEnabled;
|
|
301
|
+
}
|
|
302
|
+
export async function navigateTo(path, options = {}) {
|
|
303
|
+
const { replace = false } = options;
|
|
304
|
+
if (!spaEnabled || !spaNavigationCallback) {
|
|
305
|
+
if (replace) {
|
|
306
|
+
window.location.replace(path);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
window.location.href = path;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
if (replace) {
|
|
315
|
+
window.history.replaceState({ path }, '', path);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
window.history.pushState({ path }, '', path);
|
|
319
|
+
}
|
|
320
|
+
await spaNavigationCallback(path);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error('[metaowl] SPA navigation failed:', error);
|
|
325
|
+
window.location.href = path;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function navigate(path, options = {}) {
|
|
330
|
+
const { replace = false, reload = true } = options;
|
|
331
|
+
if (reload || !spaEnabled) {
|
|
332
|
+
if (replace) {
|
|
333
|
+
window.location.replace(path);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
window.location.href = path;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
void navigateTo(path, { replace });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function push(path) {
|
|
344
|
+
void navigateTo(path, { replace: false });
|
|
345
|
+
}
|
|
346
|
+
export function replace(path) {
|
|
347
|
+
void navigateTo(path, { replace: true });
|
|
348
|
+
}
|
|
349
|
+
export function back() {
|
|
350
|
+
window.history.back();
|
|
351
|
+
}
|
|
352
|
+
export function forward() {
|
|
353
|
+
window.history.forward();
|
|
354
|
+
}
|
|
355
|
+
export function go(n) {
|
|
356
|
+
window.history.go(n);
|
|
357
|
+
}
|
|
358
|
+
export const router = {
|
|
359
|
+
beforeEach,
|
|
360
|
+
afterEach,
|
|
361
|
+
get currentRoute() { return getCurrentRoute(); },
|
|
362
|
+
get previousRoute() { return getPreviousRoute(); },
|
|
363
|
+
get isNavigating() { return isNavigating(); },
|
|
364
|
+
cancel: cancelNavigation,
|
|
365
|
+
push,
|
|
366
|
+
replace,
|
|
367
|
+
back,
|
|
368
|
+
forward,
|
|
369
|
+
go,
|
|
370
|
+
navigate,
|
|
371
|
+
navigateTo,
|
|
372
|
+
setSpaMode,
|
|
373
|
+
isSpaMode
|
|
374
|
+
};
|
|
375
|
+
function injectSystemRoutes(route, path) {
|
|
376
|
+
if (path === '/') {
|
|
377
|
+
if (!route.path.includes('/index.html'))
|
|
378
|
+
route.path.push('/index.html');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
if (!route.path.includes(`${path}.html`))
|
|
382
|
+
route.path.push(`${path}.html`);
|
|
383
|
+
if (!route.path.includes(`${path}/`))
|
|
384
|
+
route.path.push(`${path}/`);
|
|
385
|
+
if (!route.path.includes(`${path}/index.html`))
|
|
386
|
+
route.path.push(`${path}/index.html`);
|
|
387
|
+
}
|
|
388
|
+
return route;
|
|
389
|
+
}
|