Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
- chanmilee-fe
- chasinhues
- chensokheng
- ChihebBENCHEIKH1
- chr33s
- chrille0313
- chrisngobanh
Expand Down
95 changes: 95 additions & 0 deletions integration/typegen-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,101 @@ test.describe("typegen", () => {
await $("pnpm typecheck");
});
});

// https://github.com/remix-run/react-router/issues/14676
test.describe("arrow function route components", () => {
test("ServerComponent as arrow function typechecks", async ({
edit,
$,
}) => {
await edit({
"vite.config.ts": viteConfig({ rsc: true }),
"app/routes.ts": tsx`
import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
route("server-component/:id", "routes/server-component.tsx")
] satisfies RouteConfig;
`,
"app/routes/server-component.tsx": tsx`
import type { Expect, Equal } from "../expect-type"
import type { Route } from "./+types/server-component"

export function loader({ params }: Route.LoaderArgs) {
type Test = Expect<Equal<typeof params, { id: string} >>
return { server: "server" }
}

export function action() {
return { server: "server" }
}

export const ServerComponent = ({
loaderData,
actionData,
}: Route.ServerComponentProps) => {
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string }>>
type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>
return (
<>
<h1>ServerComponent</h1>
<p>Loader data: {loaderData.server}</p>
<p>Action data: {actionData?.server}</p>
</>
)
}
`,
});
await $("pnpm typecheck");
});

test("default (Component) as arrow function typechecks", async ({
edit,
$,
}) => {
await edit({
"vite.config.ts": viteConfig({ rsc: false }),
"app/routes.ts": tsx`
import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
route("client-component/:id", "routes/client-component.tsx")
] satisfies RouteConfig;
`,
"app/routes/client-component.tsx": tsx`
import type { Expect, Equal } from "../expect-type"
import type { Route } from "./+types/client-component"

export function loader({ params }: Route.LoaderArgs) {
type Test = Expect<Equal<typeof params, { id: string} >>
return { server: "server" }
}

export function action() {
return { server: "server" }
}

const Component = ({
loaderData,
actionData,
}: Route.ComponentProps) => {
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string }>>
type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>
return (
<>
<h1>default (Component)</h1>
<p>Loader data: {loaderData.server}</p>
{actionData && <p>Action data: {actionData.server}</p>}
</>
)
}

export default Component
`,
});
await $("pnpm typecheck");
});
});
});

test("layout without pages", async ({ edit, $ }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix generated `Route.ComponentProps` / `Route.ServerComponentProps` triggering a `TS2456` / `TS7022` circular type-alias error when used as the parameter type of an arrow-function route component export — typegen now indexes into `GetAnnotations<...>` directly instead of routing through an intermediate `type Annotations = GetAnnotations<...>` alias
48 changes: 28 additions & 20 deletions packages/react-router-dev/typegen/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,57 +287,65 @@ function getRouteAnnotations({
Babel.generate(matchesType).code +
"\n\n" +
ts`
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
// The Route type aliases below index directly into
// \`GetAnnotations<...>\` instead of going through an intermediate
// \`type Annotations = GetAnnotations<...>\` alias. The intermediate
// alias forced TypeScript to eagerly compute the full annotations
// object, which triggered a type-alias circularity error
// (TS2456 / TS7022) when \`Route.ComponentProps\` was used as the
// contextual parameter type of a \`const\` arrow-function route
// export. See https://github.com/remix-run/react-router/issues/14676
type AnnotationsInput = Info & { module: Module, matches: Matches };

export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
export type LinkDescriptors = GetAnnotations<AnnotationsInput>["LinkDescriptors"];
export type LinksFunction = GetAnnotations<AnnotationsInput>["LinksFunction"];

// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
export type MetaArgs = GetAnnotations<AnnotationsInput>["MetaArgs"];
export type MetaDescriptors = GetAnnotations<AnnotationsInput>["MetaDescriptors"];
export type MetaFunction = GetAnnotations<AnnotationsInput>["MetaFunction"];

// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
export type HeadersArgs = GetAnnotations<AnnotationsInput>["HeadersArgs"];
export type HeadersFunction = GetAnnotations<AnnotationsInput>["HeadersFunction"];

// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
export type MiddlewareFunction = GetAnnotations<AnnotationsInput>["MiddlewareFunction"];

// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
export type ClientMiddlewareFunction = GetAnnotations<AnnotationsInput>["ClientMiddlewareFunction"];

// loader
export type LoaderArgs = Annotations["LoaderArgs"];
export type LoaderArgs = GetAnnotations<AnnotationsInput>["LoaderArgs"];

// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
export type ClientLoaderArgs = GetAnnotations<AnnotationsInput>["ClientLoaderArgs"];

// action
export type ActionArgs = Annotations["ActionArgs"];
export type ActionArgs = GetAnnotations<AnnotationsInput>["ActionArgs"];

// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
export type ClientActionArgs = GetAnnotations<AnnotationsInput>["ClientActionArgs"];

// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
export type HydrateFallbackProps = GetAnnotations<AnnotationsInput>["HydrateFallbackProps"];

// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
export type ServerHydrateFallbackProps = GetAnnotations<AnnotationsInput>["ServerHydrateFallbackProps"];

// Component
export type ComponentProps = Annotations["ComponentProps"];
export type ComponentProps = GetAnnotations<AnnotationsInput>["ComponentProps"];

// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
export type ServerComponentProps = GetAnnotations<AnnotationsInput>["ServerComponentProps"];

// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
export type ErrorBoundaryProps = GetAnnotations<AnnotationsInput>["ErrorBoundaryProps"];

// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
export type ServerErrorBoundaryProps = GetAnnotations<AnnotationsInput>["ServerErrorBoundaryProps"];
}
`;
return { filename, content };
Expand Down