Repository: remix-run/react-router
File: integration/typegen-test.ts (TypeScript)

Code before:

const tsx = dedent;

const nodeBin = process.argv[0];
const reactRouterBin = "node_modules/@react-router/dev/dist/cli/index.js";
const tscBin = "node_modules/typescript/bin/tsc";

function typecheck(cwd: string) {
  const typegen = spawnSync(nodeBin, [reactRouterBin, "typegen"], { cwd });
  expect(typegen.stdout.toString()).toBe("");
  expect(typegen.stderr.toString()).toBe("");
  expect(typegen.status).toBe(0);

  return spawnSync(nodeBin, [tscBin], { cwd });
}

const viteConfig = tsx`
  import { reactRouter } from "@react-router/dev/vite";

  export default {
    plugins: [reactRouter()],
  };
`;

const assertType = tsx`
  export function assertType<T>(t: T) {}
`;

test.describe("typegen", () => {
  test("basic", async () => {
    const cwd = await createProject({
      "vite.config.ts": viteConfig,
      "app/assertType.ts": assertType,
      "app/routes.ts": tsx`
        import { type RouteConfig, route } from "@react-router/dev/routes";

        export const routes: RouteConfig = [
          route("products/:id", "routes/product.tsx")
        ]
      `,
      "app/routes/product.tsx": tsx`
        import { assertType } from "../assertType"
        import type { Route } from "./+types.product"

        export function loader({ params }: Route.LoaderArgs) {
          assertType<string>(params.id)
          return { planet: "world" }
        }

        export default function Component({ loaderData }: Route.ComponentProps) {
          assertType<string>(loaderData.planet)

Review comment (bug):
If typegen was broken there were no type errors since `skipLibCheck: true` was configured for the test fixture. `skipLibCheck: true` does make sense to avoid random type errors from dependencies. However, this caused all types from typegen to be `any`.

Previously, the `assertType` check was too loose and didn't throw any type errors when `any` was passed in. The new approach is more robust and does correctly throw type errors for `any`.

Code after:

const tsx = dedent;

const nodeBin = process.argv[0];
const reactRouterBin = "node_modules/@react-router/dev/dist/cli/index.js";
const tscBin = "node_modules/typescript/bin/tsc";

function typecheck(cwd: string) {
  const typegen = spawnSync(nodeBin, [reactRouterBin, "typegen"], { cwd });
  expect(typegen.stdout.toString()).toBe("");
  expect(typegen.stderr.toString()).toBe("");
  expect(typegen.status).toBe(0);

  return spawnSync(nodeBin, [tscBin], { cwd });
}

const viteConfig = tsx`
  import { reactRouter } from "@react-router/dev/vite";

  export default {
    plugins: [reactRouter()],
  };
`;

const expectType = tsx`
  export type Expect<T extends true> = T

  export type Equal<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false
`;

test.describe("typegen", () => {
  test("basic", async () => {
    const cwd = await createProject({
      "vite.config.ts": viteConfig,
      "app/expect-type.ts": expectType,
      "app/routes.ts": tsx`
        import { type RouteConfig, route } from "@react-router/dev/routes";

        export const routes: RouteConfig = [
          route("products/:id", "routes/product.tsx")
        ]
      `,
      "app/routes/product.tsx": tsx`
        import { Expect, Equal } from "../expect-type"
        import type { Route } from "./+types.product"

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