enhance(frontend): typed nirax (#16309)

* enhance(frontend): typed nirax

* migrate router.replace

* fix
This commit is contained in:
かっこかり 2025-07-30 12:30:35 +09:00 committed by GitHub
parent b660769288
commit 4f653f2fbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 308 additions and 52 deletions

View File

@ -495,7 +495,7 @@ function done(query?: string): boolean | void {
function settings() { function settings() {
emit('esc'); emit('esc');
router.push('settings/emoji-palette'); router.push('/settings/emoji-palette');
} }
onMounted(() => { onMounted(() => {

View File

@ -151,7 +151,7 @@ const contextmenu = computed(() => ([{
function back() { function back() {
history.value.pop(); history.value.pop();
windowRouter.replace(history.value.at(-1)!.path); windowRouter.replaceByPath(history.value.at(-1)!.path);
} }
function reload() { function reload() {
@ -163,7 +163,7 @@ function close() {
} }
function expand() { function expand() {
mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage'); mainRouter.pushByPath(windowRouter.getCurrentFullPath(), 'forcePage');
windowEl.value?.close(); windowEl.value?.close();
} }

View File

@ -186,7 +186,7 @@ function searchOnKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && searchSelectedIndex.value != null) { if (ev.key === 'Enter' && searchSelectedIndex.value != null) {
ev.preventDefault(); ev.preventDefault();
router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id); router.pushByPath(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
} else if (ev.key === 'ArrowDown') { } else if (ev.key === 'ArrowDown') {
ev.preventDefault(); ev.preventDefault();
const current = searchSelectedIndex.value ?? -1; const current = searchSelectedIndex.value ?? -1;

View File

@ -64,7 +64,7 @@ function onContextmenu(ev) {
icon: 'ti ti-player-eject', icon: 'ti ti-player-eject',
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
action: () => { action: () => {
router.push(props.to, 'forcePage'); router.pushByPath(props.to, 'forcePage');
}, },
}, { type: 'divider' }, { }, { type: 'divider' }, {
icon: 'ti ti-external-link', icon: 'ti ti-external-link',
@ -99,6 +99,6 @@ function nav(ev: MouseEvent) {
return openWindow(); return openWindow();
} }
router.push(props.to, ev.ctrlKey ? 'forcePage' : null); router.pushByPath(props.to, ev.ctrlKey ? 'forcePage' : null);
} }
</script> </script>

View File

@ -76,7 +76,7 @@ function mount() {
function back() { function back() {
const prev = tabs.value[tabs.value.length - 2]; const prev = tabs.value[tabs.value.length - 2];
tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)];
router.replace(prev.fullPath); router?.replaceByPath(prev.fullPath);
} }
router.useListener('change', ({ resolved }) => { router.useListener('change', ({ resolved }) => {

View File

@ -58,7 +58,7 @@ export type RouterEvents = {
beforeFullPath: string; beforeFullPath: string;
fullPath: string; fullPath: string;
route: RouteDef | null; route: RouteDef | null;
props: Map<string, string> | null; props: Map<string, string | boolean> | null;
}) => void; }) => void;
same: () => void; same: () => void;
}; };
@ -77,6 +77,110 @@ export type PathResolvedResult = {
}; };
}; };
//#region Path Types
type Prettify<T> = {
[K in keyof T]: T[K]
} & {};
type RemoveNever<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
} & {};
type IsPathParameter<Part extends string> = Part extends `${string}:${infer Parameter}` ? Parameter : never;
type GetPathParamKeys<Path extends string> =
Path extends `${infer A}/${infer B}`
? IsPathParameter<A> | GetPathParamKeys<B>
: IsPathParameter<Path>;
type GetPathParams<Path extends string> = Prettify<{
[Param in GetPathParamKeys<Path> as Param extends `${string}?` ? never : Param]: string;
} & {
[Param in GetPathParamKeys<Path> as Param extends `${infer OptionalParam}?` ? OptionalParam : never]?: string;
}>;
type UnwrapReadOnly<T> = T extends ReadonlyArray<infer U>
? U
: T extends Readonly<infer U>
? U
: T;
type GetPaths<Def extends RouteDef> = Def extends { path: infer Path }
? Path extends string
? Def extends { children: infer Children }
? Children extends RouteDef[]
? Path | `${Path}${FlattenAllPaths<Children>}`
: Path
: Path
: never
: never;
type FlattenAllPaths<Defs extends RouteDef[]> = GetPaths<Defs[number]>;
type GetSinglePathQuery<Def extends RouteDef, Path extends FlattenAllPaths<RouteDef[]>> = RemoveNever<
Def extends { path: infer BasePath, children: infer Children }
? BasePath extends string
? Path extends `${BasePath}${infer ChildPath}`
? Children extends RouteDef[]
? ChildPath extends FlattenAllPaths<Children>
? GetPathQuery<Children, ChildPath>
: Record<string, never>
: never
: never
: never
: Def['path'] extends Path
? Def extends { query: infer Query }
? Query extends Record<string, string>
? UnwrapReadOnly<{ [Key in keyof Query]?: string; }>
: Record<string, never>
: Record<string, never>
: Record<string, never>
>;
type GetPathQuery<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = GetSinglePathQuery<Defs[number], Path>;
type RequiredIfNotEmpty<K extends string, T extends Record<string, unknown>> = T extends Record<string, never>
? { [Key in K]?: T }
: { [Key in K]: T };
type NotRequiredIfEmpty<T extends Record<string, unknown>> = T extends Record<string, never> ? T | undefined : T;
type GetRouterOperationProps<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = NotRequiredIfEmpty<RequiredIfNotEmpty<'params', GetPathParams<Path>> & {
query?: GetPathQuery<Defs, Path>;
hash?: string;
}>;
//#endregion
function buildFullPath(args: {
path: string;
params?: Record<string, string>;
query?: Record<string, string>;
hash?: string;
}) {
let fullPath = args.path;
if (args.params) {
for (const key in args.params) {
const value = args.params[key];
const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g');
fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : '');
}
}
if (args.query) {
const queryString = new URLSearchParams(args.query).toString();
if (queryString) {
fullPath += '?' + queryString;
}
}
if (args.hash) {
fullPath += '#' + encodeURIComponent(args.hash);
}
return fullPath;
}
function parsePath(path: string): ParsedPath { function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath; const res = [] as ParsedPath;
@ -282,7 +386,7 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
} }
} }
if (res.route.loginRequired && !this.isLoggedIn) { if (res.route.loginRequired && !this.isLoggedIn && 'component' in res.route) {
res.route.component = this.notFoundPageComponent; res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true); res.props.set('showLoginPopup', true);
} }
@ -310,14 +414,35 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
return this.currentFullPath; return this.currentFullPath;
} }
public push(fullPath: string, flag?: RouterFlag) { public push<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>, flag?: RouterFlag | null) {
const fullPath = buildFullPath({
path,
params: props?.params,
query: props?.query,
hash: props?.hash,
});
this.pushByPath(fullPath, flag);
}
public replace<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>) {
const fullPath = buildFullPath({
path,
params: props?.params,
query: props?.query,
hash: props?.hash,
});
this.replaceByPath(fullPath);
}
/** どうしても必要な場合に使用(パスが確定している場合は `Nirax.push` を使用すること) */
public pushByPath(fullPath: string, flag?: RouterFlag | null) {
const beforeFullPath = this.currentFullPath; const beforeFullPath = this.currentFullPath;
if (fullPath === beforeFullPath) { if (fullPath === beforeFullPath) {
this.emit('same'); this.emit('same');
return; return;
} }
if (this.navHook) { if (this.navHook) {
const cancel = this.navHook(fullPath, flag); const cancel = this.navHook(fullPath, flag ?? undefined);
if (cancel) return; if (cancel) return;
} }
const res = this.navigate(fullPath); const res = this.navigate(fullPath);
@ -333,14 +458,15 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
} }
} }
public replace(fullPath: string) { /** どうしても必要な場合に使用(パスが確定している場合は `Nirax.replace` を使用すること) */
public replaceByPath(fullPath: string) {
const res = this.navigate(fullPath); const res = this.navigate(fullPath);
this.emit('replace', { this.emit('replace', {
fullPath: res._parsedRoute.fullPath, fullPath: res._parsedRoute.fullPath,
}); });
} }
public useListener<E extends keyof RouterEvents, L = RouterEvents[E]>(event: E, listener: L) { public useListener<E extends keyof RouterEvents>(event: E, listener: EventEmitter.EventListener<RouterEvents, E>) {
this.addListener(event, listener); this.addListener(event, listener);
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -72,12 +72,20 @@ async function save() {
roleId: role.value.id, roleId: role.value.id,
...data.value, ...data.value,
}); });
router.push('/admin/roles/' + role.value.id); router.push('/admin/roles/:id', {
params: {
id: role.value.id,
}
});
} else { } else {
const created = await os.apiWithDialog('admin/roles/create', { const created = await os.apiWithDialog('admin/roles/create', {
...data.value, ...data.value,
}); });
router.push('/admin/roles/' + created.id); router.push('/admin/roles/:id', {
params: {
id: created.id,
}
});
} }
} }

View File

@ -88,7 +88,11 @@ const role = reactive(await misskeyApi('admin/roles/show', {
})); }));
function edit() { function edit() {
router.push('/admin/roles/' + role.id + '/edit'); router.push('/admin/roles/:id/edit', {
params: {
id: role.id,
}
});
} }
async function del() { async function del() {

View File

@ -47,7 +47,11 @@ async function timetravel() {
} }
function settings() { function settings() {
router.push(`/my/antennas/${props.antennaId}`); router.push('/my/antennas/:antennaId', {
params: {
antennaId: props.antennaId,
}
});
} }
function focus() { function focus() {

View File

@ -165,7 +165,11 @@ function save() {
os.apiWithDialog('channels/update', params); os.apiWithDialog('channels/update', params);
} else { } else {
os.apiWithDialog('channels/create', params).then(created => { os.apiWithDialog('channels/create', params).then(created => {
router.push(`/channels/${created.id}`); router.push('/channels/:channelId', {
params: {
channelId: created.id,
},
});
}); });
} }
} }

View File

@ -147,7 +147,11 @@ watch(() => props.channelId, async () => {
}, { immediate: true }); }, { immediate: true });
function edit() { function edit() {
router.push(`/channels/${channel.value?.id}/edit`); router.push('/channels/:channelId/edit', {
params: {
channelId: props.channelId,
}
});
} }
function openPostForm() { function openPostForm() {

View File

@ -86,7 +86,11 @@ function start(ev: MouseEvent) {
async function startUser() { async function startUser() {
// TODO: localOnly // TODO: localOnly
os.selectUser({ localOnly: true }).then(user => { os.selectUser({ localOnly: true }).then(user => {
router.push(`/chat/user/${user.id}`); router.push('/chat/user/:userId', {
params: {
userId: user.id,
}
});
}); });
} }
@ -101,7 +105,11 @@ async function createRoom() {
name: result, name: result,
}); });
router.push(`/chat/room/${room.id}`); router.push('/chat/room/:roomId', {
params: {
roomId: room.id,
}
});
} }
async function search() { async function search() {

View File

@ -61,7 +61,11 @@ async function join(invitation: Misskey.entities.ChatRoomInvitation) {
roomId: invitation.room.id, roomId: invitation.room.id,
}); });
router.push(`/chat/room/${invitation.room.id}`); router.push('/chat/room/:roomId', {
params: {
roomId: invitation.room.id,
},
});
} }
async function ignore(invitation: Misskey.entities.ChatRoomInvitation) { async function ignore(invitation: Misskey.entities.ChatRoomInvitation) {

View File

@ -429,7 +429,11 @@ async function save() {
script: script.value, script: script.value,
visibility: visibility.value, visibility: visibility.value,
}); });
router.push('/play/' + created.id + '/edit'); router.push('/play/:id/edit', {
params: {
id: created.id,
},
});
} }
} }

View File

@ -85,7 +85,11 @@ async function save() {
fileIds: files.value.map(file => file.id), fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value, isSensitive: isSensitive.value,
}); });
router.push(`/gallery/${props.postId}`); router.push('/gallery/:postId', {
params: {
postId: props.postId,
}
});
} else { } else {
const created = await os.apiWithDialog('gallery/posts/create', { const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value, title: title.value,
@ -93,7 +97,11 @@ async function save() {
fileIds: files.value.map(file => file.id), fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value, isSensitive: isSensitive.value,
}); });
router.push(`/gallery/${created.id}`); router.push('/gallery/:postId', {
params: {
postId: created.id,
}
});
} }
} }

View File

@ -150,7 +150,11 @@ async function unlike() {
} }
function edit() { function edit() {
router.push(`/gallery/${post.value.id}/edit`); router.push('/gallery/:postId/edit', {
params: {
postId: props.postId,
},
});
} }
async function reportAbuse() { async function reportAbuse() {

View File

@ -45,11 +45,20 @@ function fetch() {
promise = misskeyApi('ap/show', { promise = misskeyApi('ap/show', {
uri, uri,
}); });
promise.then(res => { promise.then(res => {
if (res.type === 'User') { if (res.type === 'User') {
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`); mainRouter.replace('/@:acct/:page?', {
params: {
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
}
});
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
mainRouter.replace(`/notes/${res.object.id}`); mainRouter.replace('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
}
});
} else { } else {
os.alert({ os.alert({
type: 'error', type: 'error',
@ -63,7 +72,11 @@ function fetch() {
} }
promise = misskeyApi('users/show', Misskey.acct.parse(uri)); promise = misskeyApi('users/show', Misskey.acct.parse(uri));
promise.then(user => { promise.then(user => {
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`); mainRouter.replace('/@:acct/:page?', {
params: {
acct: user.host != null ? `${user.username}@${user.host}` : user.username,
}
});
}); });
} }

View File

@ -154,7 +154,11 @@ async function save() {
pageId.value = created.id; pageId.value = created.id;
currentName.value = name.value.trim(); currentName.value = name.value.trim();
mainRouter.replace(`/pages/edit/${pageId.value}`); mainRouter.replace('/pages/edit/:initPageId', {
params: {
initPageId: pageId.value,
},
});
} }
} }
@ -189,7 +193,11 @@ async function duplicate() {
pageId.value = created.id; pageId.value = created.id;
currentName.value = name.value.trim(); currentName.value = name.value.trim();
mainRouter.push(`/pages/edit/${pageId.value}`); mainRouter.push('/pages/edit/:initPageId', {
params: {
initPageId: pageId.value,
},
});
} }
async function add() { async function add() {

View File

@ -267,7 +267,11 @@ function showMenu(ev: MouseEvent) {
menuItems.push({ menuItems.push({
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.edit, text: i18n.ts.edit,
action: () => router.push(`/pages/edit/${page.value.id}`), action: () => router.push('/pages/edit/:initPageId', {
params: {
initPageId: page.value!.id,
},
}),
}); });
if ($i.pinnedPageId === page.value.id) { if ($i.pinnedPageId === page.value.id) {

View File

@ -168,7 +168,11 @@ function startGame(game: Misskey.entities.ReversiGameDetailed) {
playbackRate: 1, playbackRate: 1,
}); });
router.push(`/reversi/g/${game.id}`); router.push('/reversi/g/:gameId', {
params: {
gameId: game.id,
},
});
} }
async function matchHeatbeat() { async function matchHeatbeat() {

View File

@ -264,10 +264,18 @@ async function search() {
const res = await apLookup(searchParams.value.query); const res = await apLookup(searchParams.value.query);
if (res.type === 'User') { if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`); router.push('/@:acct/:page?', {
params: {
acct: `${res.object.username}@${res.object.host}`,
},
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`); router.push('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
},
});
} }
return; return;
@ -282,7 +290,7 @@ async function search() {
text: i18n.ts.lookupConfirm, text: i18n.ts.lookupConfirm,
}); });
if (!confirm.canceled) { if (!confirm.canceled) {
router.push(`/${searchParams.value.query}`); router.pushByPath(`/${searchParams.value.query}`);
return; return;
} }
} }
@ -293,7 +301,11 @@ async function search() {
text: i18n.ts.openTagPageConfirm, text: i18n.ts.openTagPageConfirm,
}); });
if (!confirm.canceled) { if (!confirm.canceled) {
router.push(`/tags/${encodeURIComponent(searchParams.value.query.substring(1))}`); router.push('/tags/:tag', {
params: {
tag: searchParams.value.query.substring(1),
},
});
return; return;
} }
} }

View File

@ -77,10 +77,18 @@ async function search() {
const res = await promise; const res = await promise;
if (res.type === 'User') { if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`); router.push('/@:acct/:page?', {
params: {
acct: `${res.object.username}@${res.object.host}`,
},
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`); router.push('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
},
});
} }
return; return;
@ -95,7 +103,7 @@ async function search() {
text: i18n.ts.lookupConfirm, text: i18n.ts.lookupConfirm,
}); });
if (!confirm.canceled) { if (!confirm.canceled) {
router.push(`/${query}`); router.pushByPath(`/${query}`);
return; return;
} }
} }
@ -106,7 +114,11 @@ async function search() {
text: i18n.ts.openTagPageConfirm, text: i18n.ts.openTagPageConfirm,
}); });
if (!confirm.canceled) { if (!confirm.canceled) {
router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`); router.push('/user-tags/:tag', {
params: {
tag: query.substring(1),
},
});
return; return;
} }
} }

View File

@ -135,7 +135,7 @@ async function del(): Promise<void> {
webhookId: props.webhookId, webhookId: props.webhookId,
}); });
router.push('/settings/webhook'); router.push('/settings/connect');
} }
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> { async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {

View File

@ -42,7 +42,11 @@ watch(() => props.listId, async () => {
}, { immediate: true }); }, { immediate: true });
function settings() { function settings() {
router.push(`/my/lists/${props.listId}`); router.push('/my/lists/:listId', {
params: {
listId: props.listId,
}
});
} }
const headerActions = computed(() => list.value ? [{ const headerActions = computed(() => list.value ? [{

View File

@ -603,4 +603,4 @@ export const ROUTE_DEF = [{
}, { }, {
path: '/:(*)', path: '/:(*)',
component: page(() => import('@/pages/not-found.vue')), component: page(() => import('@/pages/not-found.vue')),
}] satisfies RouteDef[]; }] as const satisfies RouteDef[];

View File

@ -20,7 +20,7 @@ export function createRouter(fullPath: string): Router {
export const mainRouter = createRouter(window.location.pathname + window.location.search + window.location.hash); export const mainRouter = createRouter(window.location.pathname + window.location.search + window.location.hash);
window.addEventListener('popstate', (event) => { window.addEventListener('popstate', (event) => {
mainRouter.replace(window.location.pathname + window.location.search + window.location.hash); mainRouter.replaceByPath(window.location.pathname + window.location.search + window.location.hash);
}); });
mainRouter.addListener('push', ctx => { mainRouter.addListener('push', ctx => {

View File

@ -43,7 +43,7 @@ export function swInject() {
if (mainRouter.currentRoute.value.path === ev.data.url) { if (mainRouter.currentRoute.value.path === ev.data.url) {
return window.scroll({ top: 0, behavior: 'smooth' }); return window.scroll({ top: 0, behavior: 'smooth' });
} }
return mainRouter.push(ev.data.url); return mainRouter.pushByPath(ev.data.url);
default: default:
return; return;
} }

View File

@ -158,7 +158,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
icon: 'ti ti-user-exclamation', icon: 'ti ti-user-exclamation',
text: i18n.ts.moderation, text: i18n.ts.moderation,
action: () => { action: () => {
router.push(`/admin/user/${user.id}`); router.push('/admin/user/:userId', {
params: {
userId: user.id,
},
});
}, },
}, { type: 'divider' }); }, { type: 'divider' });
} }
@ -216,7 +220,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
icon: 'ti ti-search', icon: 'ti ti-search',
text: i18n.ts.searchThisUsersNotes, text: i18n.ts.searchThisUsersNotes,
action: () => { action: () => {
router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); router.push('/search', {
query: {
username: user.username,
host: user.host ?? undefined,
},
});
}, },
}); });
} }

View File

@ -19,12 +19,16 @@ export async function lookup(router?: Router) {
if (canceled || query.length <= 1) return; if (canceled || query.length <= 1) return;
if (query.startsWith('@') && !query.includes(' ')) { if (query.startsWith('@') && !query.includes(' ')) {
_router.push(`/${query}`); _router.pushByPath(`/${query}`);
return; return;
} }
if (query.startsWith('#')) { if (query.startsWith('#')) {
_router.push(`/tags/${encodeURIComponent(query.substring(1))}`); _router.push('/tags/:tag', {
params: {
tag: query.substring(1),
}
});
return; return;
} }
@ -32,9 +36,17 @@ export async function lookup(router?: Router) {
const res = await apLookup(query); const res = await apLookup(query);
if (res.type === 'User') { if (res.type === 'User') {
_router.push(`/@${res.object.username}@${res.object.host}`); _router.push('/@:acct/:page?', {
params: {
acct: `${res.object.username}@${res.object.host}`,
},
});
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
_router.push(`/notes/${res.object.id}`); _router.push('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
},
});
} }
return; return;