refactor(frontend): router refactoring

This commit is contained in:
syuilo 2025-03-19 15:54:30 +09:00
parent 7d4045e8b4
commit 409cd4fbd3
57 changed files with 433 additions and 632 deletions

View File

@ -273,7 +273,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
query?: Record<string, string>;
loginRequired?: boolean;
hash?: string;
globalCacheKey?: string;
children?: RouteDef[];
}
```

View File

@ -26,8 +26,6 @@ import { deckStore } from '@/ui/deck/deck-store.js';
import { analytics, initAnalytics } from '@/analytics.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/main.js';
import { createMainRouter } from '@/router/definition.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
@ -267,8 +265,6 @@ export async function common(createVue: () => App<Element>) {
const app = createVue();
setupRouter(app, createMainRouter);
if (_DEV_) {
app.config.performance = true;
}

View File

@ -24,7 +24,7 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
import { initializeSw } from '@/utility/initialize-sw.js';
import { emojiPicker } from '@/utility/emoji-picker.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { makeHotkey } from '@/utility/hotkey.js';
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';

View File

@ -88,9 +88,9 @@ import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
import RouterView from '@/components/global/RouterView.vue';
import { useRouterFactory } from '@/router/supplier';
import MkTextarea from '@/components/MkTextarea.vue';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { createRouter } from '@/router.js';
const props = defineProps<{
report: Misskey.entities.AdminAbuseUserReportsResponse[number];
@ -100,10 +100,9 @@ const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
const routerFactory = useRouterFactory();
const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
const targetRouter = createRouter(`/admin/user/${props.report.targetUserId}`);
targetRouter.init();
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
const reporterRouter = createRouter(`/admin/user/${props.report.reporterId}`);
reporterRouter.init();
const moderationNote = ref(props.report.moderationNote ?? '');

View File

@ -47,7 +47,7 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { deviceKind } from '@/utility/device-kind.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -41,8 +41,7 @@ import { i18n } from '@/i18n.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/utility/achievements.js';
import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js';
import { createRouter, mainRouter } from '@/router.js';
import { analytics } from '@/analytics.js';
import { DI } from '@/di.js';
import { prefer } from '@/preferences.js';
@ -55,8 +54,7 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const routerFactory = useRouterFactory();
const windowRouter = routerFactory(props.initialPath);
const windowRouter = createRouter(props.initialPath);
const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();

View File

@ -98,7 +98,7 @@ import type { SearchIndexItem } from '@/utility/autogen/settings-search-index.js
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@@/js/scroll.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import { initIntlString, compareStringIncludes } from '@/utility/intl-string.js';
const props = defineProps<{

View File

@ -19,7 +19,7 @@ import { url } from '@@/js/config.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const props = withDefaults(defineProps<{
to: string;

View File

@ -0,0 +1,340 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// NIRAX --- A lightweight router
import { onMounted, shallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import type { Component, ShallowRef } from 'vue';
function safeURIDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
interface RouteDefBase {
path: string;
query?: Record<string, string>;
loginRequired?: boolean;
name?: string;
hash?: string;
children?: RouteDef[];
}
interface RouteDefWithComponent extends RouteDefBase {
component: Component,
}
interface RouteDefWithRedirect extends RouteDefBase {
redirect: string | ((props: Map<string, string | boolean>) => string);
}
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
export type RouterFlag = 'forcePage';
type ParsedPath = (string | {
name: string;
startsWith?: string;
wildcard?: boolean;
optional?: boolean;
})[];
export type RouterEvent = {
change: (ctx: {
beforePath: string;
path: string;
resolved: Resolved;
}) => void;
replace: (ctx: {
path: string;
}) => void;
push: (ctx: {
beforePath: string;
path: string;
route: RouteDef | null;
props: Map<string, string> | null;
}) => void;
same: () => void;
};
export type Resolved = {
route: RouteDef;
props: Map<string, string | boolean>;
child?: Resolved;
redirected?: boolean;
/** @internal */
_parsedRoute: {
fullPath: string;
queryString: string | null;
hash: string | null;
};
};
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
path = path.substring(1);
for (const part of path.split('/')) {
if (part.includes(':')) {
const prefix = part.substring(0, part.indexOf(':'));
const placeholder = part.substring(part.indexOf(':') + 1);
const wildcard = placeholder.includes('(*)');
const optional = placeholder.endsWith('?');
res.push({
name: placeholder.replace('(*)', '').replace('?', ''),
startsWith: prefix !== '' ? prefix : undefined,
wildcard,
optional,
});
} else if (part.length !== 0) {
res.push(part);
}
}
return res;
}
export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvent> {
private routes: DEF;
public current: Resolved;
public currentRef: ShallowRef<Resolved>;
public currentRoute: ShallowRef<RouteDef>;
private currentPath: string;
private isLoggedIn: boolean;
private notFoundPageComponent: Component;
private redirectCount = 0;
public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
constructor(routes: DEF, currentPath: Nirax<DEF>['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
super();
this.routes = routes;
this.current = this.resolve(currentPath)!;
this.currentRef = shallowRef(this.current);
this.currentRoute = shallowRef(this.current.route);
this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn;
this.notFoundPageComponent = notFoundPageComponent;
}
public init() {
const res = this.navigate(this.currentPath, false);
this.emit('replace', {
path: res._parsedRoute.fullPath,
});
}
public resolve(path: string): Resolved | null {
const fullPath = path;
let queryString: string | null = null;
let hash: string | null = null;
if (path[0] === '/') path = path.substring(1);
if (path.includes('#')) {
hash = path.substring(path.indexOf('#') + 1);
path = path.substring(0, path.indexOf('#'));
}
if (path.includes('?')) {
queryString = path.substring(path.indexOf('?') + 1);
path = path.substring(0, path.indexOf('?'));
}
const _parsedRoute = {
fullPath,
queryString,
hash,
};
function check(routes: RouteDef[], _parts: string[]): Resolved | null {
forEachRouteLoop:
for (const route of routes) {
let parts = [..._parts];
const props = new Map<string, string>();
pathMatchLoop:
for (const p of parsePath(route.path)) {
if (typeof p === 'string') {
if (p === parts[0]) {
parts.shift();
} else {
continue forEachRouteLoop;
}
} else {
if (parts[0] == null && !p.optional) {
continue forEachRouteLoop;
}
if (p.wildcard) {
if (parts.length !== 0) {
props.set(p.name, safeURIDecode(parts.join('/')));
parts = [];
}
break pathMatchLoop;
} else {
if (p.startsWith) {
if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop;
props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length)));
parts.shift();
} else {
if (parts[0]) {
props.set(p.name, safeURIDecode(parts[0]));
}
parts.shift();
}
}
}
}
if (parts.length === 0) {
if (route.children) {
const child = check(route.children, []);
if (child) {
return {
route,
props,
child,
_parsedRoute,
};
} else {
continue forEachRouteLoop;
}
}
if (route.hash != null && hash != null) {
props.set(route.hash, safeURIDecode(hash));
}
if (route.query != null && queryString != null) {
const queryObject = [...new URLSearchParams(queryString).entries()]
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
for (const q in route.query) {
const as = route.query[q];
if (queryObject[q]) {
props.set(as, safeURIDecode(queryObject[q]));
}
}
}
return {
route,
props,
_parsedRoute,
};
} else {
if (route.children) {
const child = check(route.children, parts);
if (child) {
return {
route,
props,
child,
_parsedRoute,
};
} else {
continue forEachRouteLoop;
}
} else {
continue forEachRouteLoop;
}
}
}
return null;
}
const _parts = path.split('/').filter(part => part.length !== 0);
return check(this.routes, _parts);
}
private navigate(path: string, emitChange = true, _redirected = false): Resolved {
const beforePath = this.currentPath;
this.currentPath = path;
const res = this.resolve(this.currentPath);
if (res == null) {
throw new Error('no route found for: ' + path);
}
if ('redirect' in res.route) {
let redirectPath: string;
if (typeof res.route.redirect === 'function') {
redirectPath = res.route.redirect(res.props);
} else {
redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
}
if (_DEV_) console.log('Redirecting to: ', redirectPath);
if (_redirected && this.redirectCount++ > 10) {
throw new Error('redirect loop detected');
}
return this.navigate(redirectPath, emitChange, true);
}
if (res.route.loginRequired && !this.isLoggedIn) {
res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true);
}
this.current = res;
this.currentRef.value = res;
this.currentRoute.value = res.route;
if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', {
beforePath,
path,
resolved: res,
});
}
this.redirectCount = 0;
return {
...res,
redirected: _redirected,
};
}
public getCurrentPath() {
return this.currentPath;
}
public push(path: string, flag?: RouterFlag) {
const beforePath = this.currentPath;
if (path === beforePath) {
this.emit('same');
return;
}
if (this.navHook) {
const cancel = this.navHook(path, flag);
if (cancel) return;
}
const res = this.navigate(path);
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', {
beforePath,
path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
});
}
}
public replace(path: string) {
const res = this.navigate(path);
this.emit('replace', {
path: res._parsedRoute.fullPath,
});
}
}

View File

@ -43,7 +43,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const isEmpty = (x: string | null) => x == null || x === '';

View File

@ -33,7 +33,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import { rolesCache } from '@/cache.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -75,7 +75,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { infoImageUrl } from '@/instance.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -295,7 +295,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const baseRoleQ = ref('');

View File

@ -32,7 +32,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));

View File

@ -99,7 +99,7 @@ import { isSupportShare } from '@/utility/navigator.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { notesSearchAvailable } from '@/utility/check-permissions.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -71,7 +71,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -86,7 +86,7 @@ import { infoImageUrl } from '@/instance.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -53,7 +53,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}

View File

@ -47,7 +47,7 @@ import MkButton from '@/components/MkButton.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -50,7 +50,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -54,7 +54,7 @@ import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -80,7 +80,7 @@ import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
import { isSupportShare } from '@/utility/navigator.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -25,7 +25,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue';
const state = ref<'fetching' | 'done'>('fetching');

View File

@ -16,7 +16,7 @@ import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { antennasCache } from '@/cache.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
const router = useRouter();

View File

@ -19,7 +19,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { antennasCache } from '@/cache.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache.js';
import { ensureSignin } from '@/i.js';
import MkPagination from '@/components/MkPagination.vue';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
const $i = ensureSignin();

View File

@ -76,7 +76,7 @@ import { selectFile } from '@/utility/select-file.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { getPageBlockList } from '@/pages/page-editor/common.js';
const props = defineProps<{

View File

@ -120,7 +120,7 @@ import { isSupportShare } from '@/utility/navigator.js';
import { instance } from '@/instance.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';

View File

@ -45,7 +45,7 @@ import MkButton from '@/components/MkButton.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -26,7 +26,7 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
const props = defineProps<{
token?: string;

View File

@ -122,7 +122,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import type { MenuItem } from '@/types/menu.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const $i = ensureSignin();

View File

@ -18,7 +18,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { useStream } from '@/stream.js';
import { ensureSignin } from '@/i.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import * as os from '@/os.js';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';

View File

@ -115,7 +115,7 @@ import MkFolder from '@/components/MkFolder.vue';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
import MkPagination from '@/components/MkPagination.vue';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import * as os from '@/os.js';
import { useInterval } from '@@/js/use-interval.js';
import { pleaseLogin } from '@/utility/please-login.js';

View File

@ -121,7 +121,7 @@ import { instance } from '@/instance.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { apLookup } from '@/utility/lookup.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkInput from '@/components/MkInput.vue';

View File

@ -37,7 +37,7 @@ import { instance } from '@/instance.js';
import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const props = withDefaults(defineProps<{
query?: string,

View File

@ -42,7 +42,7 @@ import { clearCache } from '@/utility/clear-cache.js';
import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';

View File

@ -26,7 +26,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { installPlugin } from '@/plugin.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const code = ref<string | null>(null);

View File

@ -24,7 +24,7 @@ import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const installThemeCode = ref<string | null>(null);

View File

@ -79,7 +79,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -31,7 +31,7 @@ import { scroll } from '@@/js/scroll.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
const router = useRouter();

View File

@ -181,7 +181,7 @@ import { dateString } from '@/filters/date.js';
import { confetti } from '@/utility/confetti.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js';
import MkSparkle from '@/components/MkSparkle.vue';
import { prefer } from '@/preferences.js';

View File

@ -5,8 +5,7 @@
import { defineAsyncComponent } from 'vue';
import type { AsyncComponentLoader } from 'vue';
import type { RouteDef } from '@/router.js';
import { Router } from '@/router.js';
import type { RouteDef } from '@/lib/nirax.js';
import { $i, iAmModerator } from '@/i.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
@ -17,7 +16,7 @@ export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
errorComponent: MkError,
});
const routes: RouteDef[] = [{
export const ROUTE_DEF = [{
path: '/@:username/pages/:pageName(*)',
component: page(() => import('@/pages/page.vue')),
}, {
@ -567,7 +566,6 @@ const routes: RouteDef[] = [{
name: 'index',
path: '/',
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
globalCacheKey: 'index',
}, {
// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
path: '/redirect-test',
@ -576,8 +574,4 @@ const routes: RouteDef[] = [{
}, {
path: '/:(*)',
component: page(() => import('@/pages/not-found.vue')),
}];
export function createMainRouter(path: string): Router {
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
}
}] satisfies RouteDef[];

View File

@ -3,339 +3,44 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// NIRAX --- A lightweight router
import { inject } from 'vue';
import { page } from '@/router.definition.js';
import { $i } from '@/i.js';
import { Nirax } from '@/lib/nirax.js';
import { ROUTE_DEF } from '@/router.definition.js';
import { analytics } from '@/analytics.js';
import { DI } from '@/di.js';
import { onMounted, shallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import type { Component, ShallowRef } from 'vue';
export type Router = Nirax<typeof ROUTE_DEF>;
function safeURIDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
export function createRouter(path: string): Router {
return new Nirax(ROUTE_DEF, path, !!$i, page(() => import('@/pages/not-found.vue')));
}
interface RouteDefBase {
path: string;
query?: Record<string, string>;
loginRequired?: boolean;
name?: string;
hash?: string;
globalCacheKey?: string;
children?: RouteDef[];
}
interface RouteDefWithComponent extends RouteDefBase {
component: Component,
}
interface RouteDefWithRedirect extends RouteDefBase {
redirect: string | ((props: Map<string, string | boolean>) => string);
}
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
export type RouterFlag = 'forcePage';
type ParsedPath = (string | {
name: string;
startsWith?: string;
wildcard?: boolean;
optional?: boolean;
})[];
export type RouterEvent = {
change: (ctx: {
beforePath: string;
path: string;
resolved: Resolved;
}) => void;
replace: (ctx: {
path: string;
}) => void;
push: (ctx: {
beforePath: string;
path: string;
route: RouteDef | null;
props: Map<string, string> | null;
}) => void;
same: () => void;
};
export type Resolved = {
route: RouteDef;
props: Map<string, string | boolean>;
child?: Resolved;
redirected?: boolean;
/** @internal */
_parsedRoute: {
fullPath: string;
queryString: string | null;
hash: string | null;
};
};
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
path = path.substring(1);
for (const part of path.split('/')) {
if (part.includes(':')) {
const prefix = part.substring(0, part.indexOf(':'));
const placeholder = part.substring(part.indexOf(':') + 1);
const wildcard = placeholder.includes('(*)');
const optional = placeholder.endsWith('?');
res.push({
name: placeholder.replace('(*)', '').replace('?', ''),
startsWith: prefix !== '' ? prefix : undefined,
wildcard,
optional,
});
} else if (part.length !== 0) {
res.push(part);
}
}
return res;
}
export class Router extends EventEmitter<RouterEvent> {
private routes: RouteDef[];
public current: Resolved;
public currentRef: ShallowRef<Resolved>;
public currentRoute: ShallowRef<RouteDef>;
private currentPath: string;
private isLoggedIn: boolean;
private notFoundPageComponent: Component;
private redirectCount = 0;
public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
super();
this.routes = routes;
this.current = this.resolve(currentPath)!;
this.currentRef = shallowRef(this.current);
this.currentRoute = shallowRef(this.current.route);
this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn;
this.notFoundPageComponent = notFoundPageComponent;
}
public init() {
const res = this.navigate(this.currentPath, false);
this.emit('replace', {
path: res._parsedRoute.fullPath,
});
}
public resolve(path: string): Resolved | null {
const fullPath = path;
let queryString: string | null = null;
let hash: string | null = null;
if (path[0] === '/') path = path.substring(1);
if (path.includes('#')) {
hash = path.substring(path.indexOf('#') + 1);
path = path.substring(0, path.indexOf('#'));
}
if (path.includes('?')) {
queryString = path.substring(path.indexOf('?') + 1);
path = path.substring(0, path.indexOf('?'));
}
const _parsedRoute = {
fullPath,
queryString,
hash,
};
function check(routes: RouteDef[], _parts: string[]): Resolved | null {
forEachRouteLoop:
for (const route of routes) {
let parts = [..._parts];
const props = new Map<string, string>();
pathMatchLoop:
for (const p of parsePath(route.path)) {
if (typeof p === 'string') {
if (p === parts[0]) {
parts.shift();
} else {
continue forEachRouteLoop;
}
} else {
if (parts[0] == null && !p.optional) {
continue forEachRouteLoop;
}
if (p.wildcard) {
if (parts.length !== 0) {
props.set(p.name, safeURIDecode(parts.join('/')));
parts = [];
}
break pathMatchLoop;
} else {
if (p.startsWith) {
if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop;
props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length)));
parts.shift();
} else {
if (parts[0]) {
props.set(p.name, safeURIDecode(parts[0]));
}
parts.shift();
}
}
}
}
if (parts.length === 0) {
if (route.children) {
const child = check(route.children, []);
if (child) {
return {
route,
props,
child,
_parsedRoute,
};
} else {
continue forEachRouteLoop;
}
}
if (route.hash != null && hash != null) {
props.set(route.hash, safeURIDecode(hash));
}
if (route.query != null && queryString != null) {
const queryObject = [...new URLSearchParams(queryString).entries()]
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
for (const q in route.query) {
const as = route.query[q];
if (queryObject[q]) {
props.set(as, safeURIDecode(queryObject[q]));
}
}
}
return {
route,
props,
_parsedRoute,
};
} else {
if (route.children) {
const child = check(route.children, parts);
if (child) {
return {
route,
props,
child,
_parsedRoute,
};
} else {
continue forEachRouteLoop;
}
} else {
continue forEachRouteLoop;
}
}
}
return null;
}
const _parts = path.split('/').filter(part => part.length !== 0);
return check(this.routes, _parts);
}
private navigate(path: string, emitChange = true, _redirected = false): Resolved {
const beforePath = this.currentPath;
this.currentPath = path;
const res = this.resolve(this.currentPath);
if (res == null) {
throw new Error('no route found for: ' + path);
}
if ('redirect' in res.route) {
let redirectPath: string;
if (typeof res.route.redirect === 'function') {
redirectPath = res.route.redirect(res.props);
} else {
redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
}
if (_DEV_) console.log('Redirecting to: ', redirectPath);
if (_redirected && this.redirectCount++ > 10) {
throw new Error('redirect loop detected');
}
return this.navigate(redirectPath, emitChange, true);
}
if (res.route.loginRequired && !this.isLoggedIn) {
res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true);
}
this.current = res;
this.currentRef.value = res;
this.currentRoute.value = res.route;
if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', {
beforePath,
path,
resolved: res,
});
}
this.redirectCount = 0;
return {
...res,
redirected: _redirected,
};
}
public getCurrentPath() {
return this.currentPath;
}
public push(path: string, flag?: RouterFlag) {
const beforePath = this.currentPath;
if (path === beforePath) {
this.emit('same');
return;
}
if (this.navHook) {
const cancel = this.navHook(path, flag);
if (cancel) return;
}
const res = this.navigate(path);
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', {
beforePath,
path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
});
}
}
public replace(path: string) {
const res = this.navigate(path);
this.emit('replace', {
path: res._parsedRoute.fullPath,
});
}
export const mainRouter = createRouter(location.pathname + location.search + location.hash);
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash);
});
mainRouter.addListener('push', ctx => {
window.history.pushState({ }, '', ctx.path);
});
mainRouter.addListener('replace', ctx => {
window.history.replaceState({ }, '', ctx.path);
});
mainRouter.addListener('change', ctx => {
console.log('mainRouter: change', ctx.path);
analytics.page({
path: ctx.path,
title: ctx.path,
});
});
mainRouter.init();
export function useRouter(): Router {
return inject(DI.router, null) ?? mainRouter;
}

View File

@ -1,199 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EventEmitter } from 'eventemitter3';
import type { Router, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/router.js';
import type { App, ShallowRef } from 'vue';
import { analytics } from '@/analytics.js';
/**
* {@link Router}{@link mainRouter}
* {@link Router}{@link provide}`routerFactory`
*/
export function setupRouter(app: App, routerFactory: ((path: string) => Router)): void {
app.provide('routerFactory', routerFactory);
const mainRouter = routerFactory(location.pathname + location.search + location.hash);
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash);
});
mainRouter.addListener('push', ctx => {
window.history.pushState({ }, '', ctx.path);
});
mainRouter.addListener('replace', ctx => {
window.history.replaceState({ }, '', ctx.path);
});
mainRouter.addListener('change', ctx => {
console.log('mainRouter: change', ctx.path);
analytics.page({
path: ctx.path,
title: ctx.path,
});
});
mainRouter.init();
setMainRouter(mainRouter);
}
function getMainRouter(): Router {
const router = mainRouterHolder;
if (!router) {
throw new Error('mainRouter is not found.');
}
return router;
}
/**
*
* {@link setupRouter}
*/
export function setMainRouter(router: Router) {
if (mainRouterHolder) {
throw new Error('mainRouter is already exists.');
}
mainRouterHolder = router;
}
/**
* {@link mainRouter}
* {@link mainRouter}undefinedになる期間がある
* undefined込みにしたくないのでこのクラスを緩衝材として使用する
*/
class MainRouterProxy implements Router {
private supplier: () => Router;
constructor(supplier: () => Router) {
this.supplier = supplier;
}
get current(): Resolved {
return this.supplier().current;
}
get currentRef(): ShallowRef<Resolved> {
return this.supplier().currentRef;
}
get currentRoute(): ShallowRef<RouteDef> {
return this.supplier().currentRoute;
}
get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
return this.supplier().navHook;
}
set navHook(value) {
this.supplier().navHook = value;
}
getCurrentPath(): string {
return this.supplier().getCurrentPath();
}
push(path: string, flag?: RouterFlag): void {
this.supplier().push(path, flag);
}
replace(path: string, key?: string | null): void {
this.supplier().replace(path, key);
}
resolve(path: string): Resolved | null {
return this.supplier().resolve(path);
}
init(): void {
this.supplier().init();
}
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
return this.supplier().eventNames();
}
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
): Array<EventEmitter.EventListener<RouterEvent, T>> {
return this.supplier().listeners(event);
}
listenerCount(
event: EventEmitter.EventNames<RouterEvent>,
): number {
return this.supplier().listenerCount(event);
}
emit<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
...args: EventEmitter.EventArgs<RouterEvent, T>
): boolean {
return this.supplier().emit(event, ...args);
}
on<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().on(event, fn, context);
return this;
}
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().addListener(event, fn, context);
return this;
}
once<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().once(event, fn, context);
return this;
}
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn?: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
once?: boolean,
): this {
this.supplier().removeListener(event, fn, context, once);
return this;
}
off<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn?: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
once?: boolean,
): this {
this.supplier().off(event, fn, context, once);
return this;
}
removeAllListeners(
event?: EventEmitter.EventNames<RouterEvent>,
): this {
this.supplier().removeAllListeners(event);
return this;
}
}
let mainRouterHolder: Router | null = null;
export const mainRouter: Router = new MainRouterProxy(getMainRouter);

View File

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { inject } from 'vue';
import type { Router } from '@/router.js';
import { mainRouter } from '@/router/main.js';
import { DI } from '@/di.js';
/**
* {@link Router}
* {@link setupRouter}{@link provide}{@link Router}
*/
export function useRouter(): Router {
return inject(DI.router, null) ?? mainRouter;
}
/**
* {@link Router}
* {@link setupRouter}
*/
export function useRouterFactory(): (path: string) => Router {
const factory = inject<(path: string) => Router>('routerFactory');
if (!factory) {
console.error('routerFactory is not defined.');
throw new Error('routerFactory is not defined.');
}
return factory;
}

View File

@ -97,7 +97,7 @@ import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
import { useRouter } from '@/router/supplier.js';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';

View File

@ -8,7 +8,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { $i } from '@/i.js';
import { getAccountFromId } from '@/utility/get-account-from-id.js';
import { deepClone } from '@/utility/clone.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { login } from '@/accounts.js';
export function swInject() {

View File

@ -58,7 +58,7 @@ import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';

View File

@ -114,7 +114,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));

View File

@ -28,7 +28,7 @@ import type { PageMetadata } from '@/page.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';

View File

@ -19,7 +19,7 @@ import { instanceName } from '@@/js/config.js';
import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { DI } from '@/di.js';
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');

View File

@ -110,7 +110,7 @@ import { $i } from '@/i.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { deviceKind } from '@/utility/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { DI } from '@/di.js';

View File

@ -36,7 +36,7 @@ import { instance } from '@/instance.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { i18n } from '@/i18n.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { DI } from '@/di.js';
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');

View File

@ -30,7 +30,7 @@ import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { i18n } from '@/i18n.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { DI } from '@/di.js';
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');

View File

@ -16,7 +16,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { $i, iAmModerator } from '@/i.js';
import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';

View File

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Router } from '@/router.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { Router } from '@/router.js';
import { mainRouter } from '@/router/main.js';
import { mainRouter } from '@/router.js';
export async function lookup(router?: Router) {
const _router = router ?? mainRouter;