diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c63f69cf1..a3aedfa9eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -273,7 +273,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド query?: Record; loginRequired?: boolean; hash?: string; - globalCacheKey?: string; children?: RouteDef[]; } ``` diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 10bcddbde7..a520be9f8a 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -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) { const app = createVue(); - setupRouter(app, createMainRouter); - if (_DEV_) { app.config.performance = true; } diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 46668cb934..62ee0c5d72 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -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'; diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 6892435a65..d6f65eca5d 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -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 ?? ''); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index f02a767186..c54d9eb4d5 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -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(); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index a6049b4d91..fae4246335 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -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); const windowEl = shallowRef>(); diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index fddf3934bb..a094718382 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -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<{ diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 173f6a849f..3403418991 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -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; diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts new file mode 100644 index 0000000000..cc20d497e6 --- /dev/null +++ b/packages/frontend/src/lib/nirax.ts @@ -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; + loginRequired?: boolean; + name?: string; + hash?: string; + children?: RouteDef[]; +} + +interface RouteDefWithComponent extends RouteDefBase { + component: Component, +} + +interface RouteDefWithRedirect extends RouteDefBase { + redirect: string | ((props: Map) => 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 | null; + }) => void; + same: () => void; +}; + +export type Resolved = { + route: RouteDef; + props: Map; + 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 extends EventEmitter { + private routes: DEF; + public current: Resolved; + public currentRef: ShallowRef; + public currentRoute: ShallowRef; + 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['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(); + + 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, + }); + } +} diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 7e7467859b..cdf3b4230c 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -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 === ''; diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index 129fabf489..7741064685 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -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(); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index e7ebd30a3b..631873a076 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -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(); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index e16fca8286..0428352350 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -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(''); diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 542fa72126..f0587a5ca0 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -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(); diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 80cefe12c3..d0656a163c 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -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)); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 1245561169..1419e83df7 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -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(); diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 071f5a048b..cf047fcd5d 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -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(); diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index c2f57cb665..5390a48be5 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -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(); diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index b04974b7dc..16eeba7eea 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -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} diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 3cd7c46c1e..6aee91dfda 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -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(); diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 9cd59d0aa5..c85823ba86 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -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(); diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 14b3f7bf3c..04445c913c 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -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(); diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 6a9737e30f..9d37daf1ed 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -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(); diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue index e92ee0a4cc..fafad8af4a 100644 --- a/packages/frontend/src/pages/lookup.vue +++ b/packages/frontend/src/pages/lookup.vue @@ -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'); diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 42d8b7be4c..e7460f0f93 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -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(); diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index acd368b5e2..83d1183ddd 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -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(); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 782c069839..08ac3b4625 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -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(); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index e2f6084252..95a2d6d616 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -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<{ diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 523d443359..cad5f2e109 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -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'; diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index f9bb825bd0..e41a7da055 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -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(); diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index d84c8f33dd..fda365fe52 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -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; diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 75d89be6b0..d2720a79fc 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -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(); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index fb2019ae3c..a447572cc0 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -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'; diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index d66ff8db05..e3f01d9938 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -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'; diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 4801e9bc27..1dc55d002c 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -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'; diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 40019cc870..101de6a64f 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -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, diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index f6feaee453..89dc9581c2 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -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'; diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index e984ed7f8a..22b53b4b96 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -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(null); diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 68e4bef5c4..ac95279402 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -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(null); diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 2de948c69d..6a6cec70ba 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -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(); diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index d4c7c9386d..e4857c7d30 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -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(); diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 149481f99b..dfa43e1ef2 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -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'; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router.definition.ts similarity index 98% rename from packages/frontend/src/router/definition.ts rename to packages/frontend/src/router.definition.ts index 923903ec3d..3b60ee68e3 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -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[]; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index c6f3937cde..b5f59b30c1 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -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; -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; - 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); -} - -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 | null; - }) => void; - same: () => void; -}; - -export type Resolved = { - route: RouteDef; - props: Map; - 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 { - private routes: RouteDef[]; - public current: Resolved; - public currentRef: ShallowRef; - public currentRoute: ShallowRef; - 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(); - - 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; } diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts deleted file mode 100644 index f294af059d..0000000000 --- a/packages/frontend/src/router/main.ts +++ /dev/null @@ -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 { - return this.supplier().currentRef; - } - - get currentRoute(): ShallowRef { - 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> { - return this.supplier().eventNames(); - } - - listeners>( - event: T, - ): Array> { - return this.supplier().listeners(event); - } - - listenerCount( - event: EventEmitter.EventNames, - ): number { - return this.supplier().listenerCount(event); - } - - emit>( - event: T, - ...args: EventEmitter.EventArgs - ): boolean { - return this.supplier().emit(event, ...args); - } - - on>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().on(event, fn, context); - return this; - } - - addListener>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().addListener(event, fn, context); - return this; - } - - once>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().once(event, fn, context); - return this; - } - - removeListener>( - event: T, - fn?: EventEmitter.EventListener, - context?: any, - once?: boolean, - ): this { - this.supplier().removeListener(event, fn, context, once); - return this; - } - - off>( - event: T, - fn?: EventEmitter.EventListener, - context?: any, - once?: boolean, - ): this { - this.supplier().off(event, fn, context, once); - return this; - } - - removeAllListeners( - event?: EventEmitter.EventNames, - ): this { - this.supplier().removeAllListeners(event); - return this; - } -} - -let mainRouterHolder: Router | null = null; - -export const mainRouter: Router = new MainRouterProxy(getMainRouter); diff --git a/packages/frontend/src/router/supplier.ts b/packages/frontend/src/router/supplier.ts deleted file mode 100644 index 3c05b41c20..0000000000 --- a/packages/frontend/src/router/supplier.ts +++ /dev/null @@ -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; -} diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 1810ec1743..754bf070fa 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -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'; diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index ae61e497b5..1459881ba1 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -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() { diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 969e30b3a9..fa63586ef7 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -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'; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index a5db4031e2..4e9ec7c586 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -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')); diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index b4d494fb09..78454d2e49 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -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'; diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index 7b58829ff5..4bfd240805 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -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'); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index be933d5324..a6695de39d 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -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'; diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 976c3584a7..ddc3761b04 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -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'); diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index d60611a19c..8a09ad80d9 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -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'); diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index c28b2db6d7..de20f2678e 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -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'; diff --git a/packages/frontend/src/utility/lookup.ts b/packages/frontend/src/utility/lookup.ts index e0b945e49b..90611094fa 100644 --- a/packages/frontend/src/utility/lookup.ts +++ b/packages/frontend/src/utility/lookup.ts @@ -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;