diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-07-30 12:30:35 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-30 12:30:35 +0900 |
| commit | 4f653f2fbc9f48f2d3069dd587907ebee667386c (patch) | |
| tree | 7a01cec63c94f56c5da8af3da9356ce74c265def /packages/frontend/src/lib | |
| parent | perf(frontend): draw-blurhash workerの結果をpostMessageする際にImageB... (diff) | |
| download | misskey-4f653f2fbc9f48f2d3069dd587907ebee667386c.tar.gz misskey-4f653f2fbc9f48f2d3069dd587907ebee667386c.tar.bz2 misskey-4f653f2fbc9f48f2d3069dd587907ebee667386c.zip | |
enhance(frontend): typed nirax (#16309)
* enhance(frontend): typed nirax
* migrate router.replace
* fix
Diffstat (limited to 'packages/frontend/src/lib')
| -rw-r--r-- | packages/frontend/src/lib/nirax.ts | 138 |
1 files changed, 132 insertions, 6 deletions
diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts index a166df9eb0..70db47e24e 100644 --- a/packages/frontend/src/lib/nirax.ts +++ b/packages/frontend/src/lib/nirax.ts @@ -58,7 +58,7 @@ export type RouterEvents = { beforeFullPath: string; fullPath: string; route: RouteDef | null; - props: Map<string, string> | null; + props: Map<string, string | boolean> | null; }) => 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 { 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.props.set('showLoginPopup', true); } @@ -310,14 +414,35 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> { 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; if (fullPath === beforeFullPath) { this.emit('same'); return; } if (this.navHook) { - const cancel = this.navHook(fullPath, flag); + const cancel = this.navHook(fullPath, flag ?? undefined); if (cancel) return; } 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); this.emit('replace', { 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); onBeforeUnmount(() => { |