Skip to content

Commit 65af517

Browse files
React Router type safety (#580)
Co-authored-by: me <me@kentcdodds.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent d1b1170 commit 65af517

37 files changed

Lines changed: 109 additions & 130 deletions

packages/workshop-app/app/routes/$.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1+
import { type Route } from './+types/$'
12
// This is called a "splat route" and as it's in the root `/app/routes/`
23
// directory, it's a catchall. If no other routes match, this one will and we
34
// can know that the user is hitting a URL that doesn't exist. By throwing a
45
// 404 from the loader, we can force the error boundary to render which will
56
// ensure the user gets the right status code and we can display a nicer error
67
// message for them than the Remix and/or browser default.
78

8-
import { type LoaderFunctionArgs, Link, useLocation } from 'react-router'
9+
import { Link, useLocation } from 'react-router'
910
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
1011
import { Icon } from '#app/components/icons.tsx'
1112

12-
export async function loader({ params }: LoaderFunctionArgs) {
13+
export async function loader({ params }: Route.LoaderArgs) {
1314
const splat = params['*']
1415
const segments = splat?.split('/') ?? []
1516
if (segments.length > 0 && !isNaN(Number(segments[0]))) {

packages/workshop-app/app/routes/_app+/_layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type Route } from './+types/_layout'
12
import {
23
extractNumbersAndTypeFromAppNameOrPath,
34
getApps,
@@ -24,7 +25,6 @@ import {
2425
useParams,
2526
data,
2627
type HeadersFunction,
27-
type LoaderFunctionArgs,
2828
} from 'react-router'
2929
import { useHydrated } from 'remix-utils/use-hydrated'
3030
import { Icon } from '#app/components/icons.tsx'
@@ -94,7 +94,7 @@ function getSidecarStatus() {
9494
return { processes, hasFailure, count: processes.length, failureCount }
9595
}
9696

97-
export async function loader({ request }: LoaderFunctionArgs) {
97+
export async function loader({ request }: Route.LoaderArgs) {
9898
const timings = makeTimings('appLayoutLoader')
9999
const { title: workshopTitle } = getWorkshopConfig()
100100
const [exercises, playgroundAppName, apps] = await Promise.all([

packages/workshop-app/app/routes/_app+/account.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { type Route } from './+types/account'
12
import { deleteCache } from '@epic-web/workshop-utils/cache.server'
23
import {
34
logout,
45
requireAuthInfo,
56
setPreferences,
67
} from '@epic-web/workshop-utils/db.server'
78
import { type SEOHandle } from '@nasa-gcn/remix-seo'
8-
import { redirect, type LoaderFunctionArgs, Form, Link } from 'react-router'
9+
import { redirect, Form, Link } from 'react-router'
910
import { Button } from '#app/components/button.tsx'
1011
import { Icon } from '#app/components/icons.tsx'
1112
import {
@@ -26,13 +27,13 @@ export const handle: SEOHandle = {
2627
getSitemapEntries: () => null,
2728
}
2829

29-
export async function loader({ request }: LoaderFunctionArgs) {
30+
export async function loader({ request }: Route.LoaderArgs) {
3031
ensureUndeployed()
3132
await requireAuthInfo({ request })
3233
return {}
3334
}
3435

35-
export async function action({ request }: { request: Request }) {
36+
export async function action({ request }: Route.ActionArgs) {
3637
ensureUndeployed()
3738
const formData = await request.formData()
3839
const intent = formData.get('intent')

packages/workshop-app/app/routes/_app+/app.$appName+/$.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { type Route } from './+types/$'
12
import path from 'path'
23
import { invariantResponse } from '@epic-web/invariant'
34
import { makeTimings } from '@epic-web/workshop-utils/timing.server'
45
import etag from 'etag'
56
import fsExtra from 'fs-extra'
67
import mimeTypes from 'mime-types'
7-
import { redirect, type LoaderFunctionArgs } from 'react-router'
8+
import { redirect } from 'react-router'
89
import { z } from 'zod'
910
import { compileTs } from '#app/utils/compile-app.server.ts'
1011
import {
@@ -173,7 +174,7 @@ function generateErrorJavaScript(
173174
`
174175
}
175176

176-
export async function loader({ request, params }: LoaderFunctionArgs) {
177+
export async function loader({ request, params }: Route.LoaderArgs) {
177178
ensureUndeployed()
178179
const timings = makeTimings('app-file')
179180
const { fileApp, app } = await resolveApps({ request, params, timings })

packages/workshop-app/app/routes/_app+/app.$appName+/api.$.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1+
import { type Route } from './+types/api.$'
12
import path from 'node:path'
23
import { invariantResponse } from '@epic-web/invariant'
34
import { makeTimings } from '@epic-web/workshop-utils/timing.server'
45
import fsExtra from 'fs-extra'
5-
import {
6-
redirect,
7-
type ActionFunctionArgs,
8-
type LoaderFunctionArgs,
9-
} from 'react-router'
6+
import { redirect } from 'react-router'
107
import { z } from 'zod'
118
import { compileTs } from '#app/utils/compile-app.server.ts'
129
import { ensureUndeployed, getBaseUrl } from '#app/utils/misc.tsx'
1310
import { resolveApps } from './__utils'
1411

15-
export async function loader(args: LoaderFunctionArgs) {
12+
export async function loader(args: Route.LoaderArgs) {
1613
ensureUndeployed()
1714
const api = await getApiModule(args)
1815
const loaderFn = api.mod.loader as
19-
| ((loaderArgs: LoaderFunctionArgs) => unknown)
16+
| ((loaderArgs: Route.LoaderArgs) => unknown)
2017
| undefined
2118
invariantResponse(
2219
loaderFn,
@@ -31,11 +28,11 @@ export async function loader(args: LoaderFunctionArgs) {
3128
}
3229
}
3330

34-
export async function action(args: ActionFunctionArgs) {
31+
export async function action(args: Route.ActionArgs) {
3532
ensureUndeployed()
3633
const api = await getApiModule(args)
3734
const actionFn = api.mod.action as
38-
| ((actionArgs: ActionFunctionArgs) => unknown)
35+
| ((actionArgs: Route.ActionArgs) => unknown)
3936
| undefined
4037
invariantResponse(
4138
actionFn,
@@ -55,7 +52,7 @@ const ApiModuleSchema = z.object({
5552
action: z.function().optional(),
5653
})
5754

58-
async function getApiModule({ request, params }: LoaderFunctionArgs) {
55+
async function getApiModule({ request, params }: Route.LoaderArgs) {
5956
const timings = makeTimings('app-api')
6057
const { fileApp, app } = await resolveApps({ request, params, timings })
6158
if (!fileApp || !app) {

packages/workshop-app/app/routes/_app+/app.$appName+/epic_ws[.js].ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { type Route } from './+types/epic_ws[.js]'
12
import { makeTimings } from '@epic-web/workshop-utils/timing.server'
2-
import { redirect, type LoaderFunctionArgs } from 'react-router'
3+
import { redirect } from 'react-router'
34
import { ensureUndeployed, getBaseUrl } from '#app/utils/misc.tsx'
45
import { resolveApps } from './__utils.ts'
56

6-
export async function loader({ request, params }: LoaderFunctionArgs) {
7+
export async function loader({ request, params }: Route.LoaderArgs) {
78
ensureUndeployed()
89
const timings = makeTimings('epic_ws script')
910
const { fileApp, app } = await resolveApps({ request, params, timings })

packages/workshop-app/app/routes/_app+/app.$appName+/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type Route } from './+types/index'
12
import path from 'path'
23
import {
34
getAppByName,
@@ -14,11 +15,11 @@ import {
1415
makeTimings,
1516
} from '@epic-web/workshop-utils/timing.server'
1617
import fsExtra from 'fs-extra'
17-
import { redirect, type LoaderFunctionArgs } from 'react-router'
18+
import { redirect } from 'react-router'
1819
import { ensureUndeployed, getBaseUrl } from '#app/utils/misc.tsx'
1920
import { resolveApps } from './__utils.ts'
2021

21-
export async function loader({ request, params }: LoaderFunctionArgs) {
22+
export async function loader({ request, params }: Route.LoaderArgs) {
2223
ensureUndeployed()
2324
const timings = makeTimings('app')
2425
const { fileApp, app } = await resolveApps({ request, params, timings })

packages/workshop-app/app/routes/_app+/app.$appName+/test.$testName.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type Route } from './+types/test.$testName'
12
import path from 'path'
23
import { invariantResponse } from '@epic-web/invariant'
34
import {
@@ -13,12 +14,11 @@ import {
1314
makeTimings,
1415
} from '@epic-web/workshop-utils/timing.server'
1516
import fsExtra from 'fs-extra'
16-
import { type LoaderFunctionArgs } from 'react-router'
1717
import { ensureUndeployed } from '#app/utils/misc.tsx'
1818
import { redirectWithToast } from '#app/utils/toast.server.ts'
1919
import { resolveApps } from './__utils.ts'
2020

21-
export async function loader({ request, params }: LoaderFunctionArgs) {
21+
export async function loader({ request, params }: Route.LoaderArgs) {
2222
ensureUndeployed()
2323
const timings = makeTimings('app_test_loader')
2424
const userHasAccess = await userHasAccessToWorkshop({

packages/workshop-app/app/routes/_app+/exercise+/$exerciseNumber_.$stepNumber.$type+/app.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1+
import { type Route } from './+types/app'
12
import { requireExerciseApp } from '@epic-web/workshop-utils/apps.server'
23
import {
34
combineServerTimings,
45
makeTimings,
56
} from '@epic-web/workshop-utils/timing.server'
67
import { useRef } from 'react'
7-
import {
8-
data,
9-
type HeadersFunction,
10-
type LoaderFunctionArgs,
11-
useLoaderData,
12-
} from 'react-router'
8+
import { data, type HeadersFunction, useLoaderData } from 'react-router'
139
import { type InBrowserBrowserRef } from '#app/components/in-browser-browser.ts'
1410
import { Preview } from './__shared/preview.tsx'
1511
import { getAppRunningState } from './__shared/utils.tsx'
1612

17-
export async function loader({ request, params }: LoaderFunctionArgs) {
13+
export async function loader({ request, params }: Route.LoaderArgs) {
1814
const timings = makeTimings('exercise-step-test')
1915
const exerciseStepApp = await requireExerciseApp(params, { request, timings })
2016
const { isRunning, portIsAvailable } =

packages/workshop-app/app/routes/_app+/exercise+/$exerciseNumber_.$stepNumber.$type+/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type Route } from './+types/index'
12
import {
23
getAppByName,
34
getAppDisplayName,
@@ -24,7 +25,6 @@ import {
2425
data,
2526
redirect,
2627
type HeadersFunction,
27-
type LoaderFunctionArgs,
2828
useOutletContext,
2929
} from 'react-router'
3030
import { Diff } from '#app/components/diff.tsx'
@@ -38,13 +38,12 @@ import {
3838
import { useWorkshopConfig } from '#app/components/workshop-config.tsx'
3939
import { fetchDiscordPosts } from '#app/utils/discord.server.ts'
4040
import { useAltDown } from '#app/utils/misc.tsx'
41-
import { type Route } from './+types/index.ts'
4241
import { Playground } from './__shared/playground.tsx'
4342
import { Preview } from './__shared/preview.tsx'
4443
import { Tests } from './__shared/tests.tsx'
4544
import { getAppRunningState, getTestState } from './__shared/utils.tsx'
4645

47-
export async function loader({ request, params }: LoaderFunctionArgs) {
46+
export async function loader({ request, params }: Route.LoaderArgs) {
4847
const timings = makeTimings('exerciseStepTypeIndexLoader')
4948
const searchParams = new URL(request.url).searchParams
5049
const cacheOptions = { request, timings }

0 commit comments

Comments
 (0)