Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): props inferred for page components #917

Merged
merged 9 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions docs/create-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const pages = createPages(async ({ createPage, createLayout }) => [
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<typeof pages>;
pages: typeof pages;
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
8 changes: 3 additions & 5 deletions examples/07_router/src/entries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const pages = createPages(async ({ createPage, createLayout }) => [
render: 'static',
path: '/nested/[id]',
staticPaths: ['foo', 'bar'],
component: ({ id }: { id: string }) => (
component: ({ id }) => (
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved
<>
<h2>Nested</h2>
<h3>Static: {id}</h3>
Expand All @@ -72,7 +72,7 @@ const pages = createPages(async ({ createPage, createLayout }) => [
createPage({
render: 'dynamic',
path: '/nested/[id]',
component: ({ id }: { id: string }) => (
component: ({ id }) => (
<>
<h2>Nested</h2>
<h3>Dynamic: {id}</h3>
Expand All @@ -83,9 +83,7 @@ const pages = createPages(async ({ createPage, createLayout }) => [
createPage({
render: 'dynamic',
path: '/any/[...all]',
component: ({ all }: { all: string[] }) => (
<h2>Catch-all: {all.join('/')}</h2>
),
component: ({ all }) => <h2>Catch-all: {all.join('/')}</h2>,
}),

// Custom Not Found page
Expand Down
7 changes: 7 additions & 0 deletions packages/waku/src/router/base-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type {
PagePath,
PropsForPages,
} from './create-pages-utils/inferred-path-types.js';

export type { PathsForPages } from './create-pages-utils/inferred-path-types.js';
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface RouteConfig {
// routes to be overridden by users
}

export type PageProps<Path extends PagePath<RouteConfig>> = PropsForPages<Path>;
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion packages/waku/src/router/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ import {
import type { RouteProps, ShouldSkip } from './common.js';
import type { RouteConfig } from './base-types.js';

type InferredPaths = RouteConfig extends { paths: infer UserPaths }
type InferredPaths = RouteConfig extends {
paths: { expanded: infer UserPaths };
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved
}
? UserPaths
: string;

Expand Down
4 changes: 2 additions & 2 deletions packages/waku/src/router/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type RouteProps = {
path: string;
export type RouteProps<Path extends string = string> = {
path: Path;
query: string;
hash: string;
};
tylersayshi marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
53 changes: 50 additions & 3 deletions packages/waku/src/router/create-pages-utils/inferred-path-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RouteProps } from '../common.js';
import type { PathWithoutSlug } from '../create-pages.js';
import type { Join, ReplaceAll, Split } from '../util-types.js';
import type { Join, ReplaceAll, Split, Prettify } from '../util-types.js';

type ReadOnlyStringTupleList = readonly (readonly string[])[];

Expand Down Expand Up @@ -140,7 +141,53 @@ export type AnyPage = {
* type MyPaths = PathsForPages<typeof pages>;
* // type MyPaths = '/foo' | '/bar';
*/
export type PathsForPages<PagesResult extends { DO_NOT_USE_pages: AnyPage }> =
CollectPaths<PagesResult['DO_NOT_USE_pages']> extends never
export type PathsForPages<PagesResult extends { DO_NOT_USE_pages: AnyPage }> = {
/** All possible paths with string or static paths in place of slug */
expanded: CollectPaths<PagesResult['DO_NOT_USE_pages']> extends never
? string
: CollectPaths<PagesResult['DO_NOT_USE_pages']> & {};
/** Paths with slugs as string literals */
initial: PagesResult['DO_NOT_USE_pages']['path'];
};

type _GetSlugs<
Route extends string,
SplitRoute extends string[] = Split<Route, '/'>,
Result extends string[] = [],
> = SplitRoute extends []
? Result
: SplitRoute extends [`${infer MaybeSlug}`, ...infer Rest extends string[]]
? MaybeSlug extends `[${infer Slug}]`
? _GetSlugs<Route, Rest, [...Result, Slug]>
: _GetSlugs<Route, Rest, Result>
: Result;

export type GetSlugs<Route extends string> = _GetSlugs<Route>;

export type PagePath<Config> = Config extends {
paths: {
initial: infer AllPages;
};
}
? AllPages
: never;

type IndividualSlugType<Slug extends string> = Slug extends `...${string}`
? string[]
: string;

type CleanWildcard<Slug extends string> = Slug extends `...${infer Wildcard}`
? Wildcard
: Slug;

type SlugTypes<Path extends string> =
GetSlugs<Path> extends string[]
? {
[Slug in GetSlugs<Path>[number] as CleanWildcard<Slug>]: IndividualSlugType<Slug>;
}
: never;

export type PropsForPages<Path extends string> = Prettify<
Omit<RouteProps<ReplaceAll<Path, `[${string}]`, string>>, 'hash'> &
SlugTypes<Path>
>;
31 changes: 9 additions & 22 deletions packages/waku/src/router/create-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import {
path2regexp,
} from '../lib/utils/path.js';
import type { PathSpec } from '../lib/utils/path.js';
import type { Split } from './util-types.js';
import type { AnyPage } from './create-pages-utils/inferred-path-types.js';
import type {
AnyPage,
GetSlugs,
PropsForPages,
} from './create-pages-utils/inferred-path-types.js';

const hasPathSpecPrefix = (prefix: PathSpec, path: PathSpec) => {
for (let i = 0; i < prefix.length; i++) {
Expand Down Expand Up @@ -67,20 +70,6 @@ export type PathWithSlug<T, K extends string> =
: never
: never;

type _GetSlugs<
Route extends string,
SplitRoute extends string[] = Split<Route, '/'>,
Result extends string[] = [],
> = SplitRoute extends []
? Result
: SplitRoute extends [`${infer MaybeSlug}`, ...infer Rest extends string[]]
? MaybeSlug extends `[${infer Slug}]`
? _GetSlugs<Route, Rest, [...Result, Slug]>
: _GetSlugs<Route, Rest, Result>
: Result;

export type GetSlugs<Route extends string> = _GetSlugs<Route>;

export type StaticSlugRoutePathsTuple<
T extends string,
Slugs extends unknown[] = GetSlugs<T>,
Expand Down Expand Up @@ -130,25 +119,23 @@ export type CreatePage = <
| {
render: Extract<Render, 'static'>;
path: PathWithoutSlug<Path>;
component: FunctionComponent<RouteProps>;
component: FunctionComponent<PropsForPages<Path>>;
}
| {
render: Extract<Render, 'static'>;
path: PathWithStaticSlugs<Path>;
staticPaths: StaticPaths;
component: FunctionComponent<RouteProps & Record<SlugKey, string>>;
component: FunctionComponent<PropsForPages<Path>>;
}
| {
render: Extract<Render, 'dynamic'>;
path: PathWithoutSlug<Path>;
component: FunctionComponent<RouteProps>;
component: FunctionComponent<PropsForPages<Path>>;
}
| {
render: Extract<Render, 'dynamic'>;
path: PathWithWildcard<Path, SlugKey, WildSlugKey>;
component: FunctionComponent<
RouteProps & Record<SlugKey, string> & Record<WildSlugKey, string[]>
>;
component: FunctionComponent<PropsForPages<Path>>;
}
) & { unstable_disableSSR?: boolean },
) => Omit<
Expand Down
Loading