@remix-run/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  2. package/dist/lib/bootstrap-project.js +32 -9
  3. package/dist/lib/cli.d.ts +29 -0
  4. package/dist/lib/cli.d.ts.map +1 -1
  5. package/dist/lib/cli.js +26 -4
  6. package/dist/lib/commands/completion.d.ts.map +1 -1
  7. package/dist/lib/commands/completion.js +5 -1
  8. package/dist/lib/commands/doctor.js +18 -18
  9. package/dist/lib/commands/routes.js +3 -3
  10. package/dist/lib/completion.js +1 -5
  11. package/dist/lib/controller-files.d.ts +0 -1
  12. package/dist/lib/controller-files.d.ts.map +1 -1
  13. package/dist/lib/controller-files.js +3 -5
  14. package/dist/lib/controller-ownership.d.ts +9 -9
  15. package/dist/lib/controller-ownership.d.ts.map +1 -1
  16. package/dist/lib/controller-ownership.js +56 -91
  17. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  18. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  19. package/dist/lib/doctor/controller-findings.js +15 -87
  20. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  21. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  22. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  23. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  24. package/dist/lib/doctor/controller-placeholders.js +18 -149
  25. package/dist/lib/doctor/controllers.js +1 -1
  26. package/dist/lib/doctor/project.js +60 -52
  27. package/dist/lib/doctor/types.d.ts +2 -2
  28. package/dist/lib/doctor/types.d.ts.map +1 -1
  29. package/dist/lib/help-text.js +1 -1
  30. package/dist/lib/load-route-map-worker.js +17 -9
  31. package/dist/lib/route-map.d.ts +1 -1
  32. package/dist/lib/route-map.d.ts.map +1 -1
  33. package/dist/lib/route-map.js +29 -17
  34. package/package.json +5 -5
  35. package/src/lib/bootstrap-project.ts +39 -13
  36. package/src/lib/cli.ts +39 -4
  37. package/src/lib/commands/completion.ts +6 -1
  38. package/src/lib/commands/doctor.ts +18 -21
  39. package/src/lib/commands/routes.ts +3 -3
  40. package/src/lib/completion.ts +1 -5
  41. package/src/lib/controller-files.ts +4 -8
  42. package/src/lib/controller-ownership.ts +78 -141
  43. package/src/lib/doctor/controller-findings.ts +20 -97
  44. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  45. package/src/lib/doctor/controller-placeholders.ts +17 -189
  46. package/src/lib/doctor/controllers.ts +1 -1
  47. package/src/lib/doctor/project.ts +60 -52
  48. package/src/lib/doctor/types.ts +1 -5
  49. package/src/lib/help-text.ts +3 -2
  50. package/src/lib/load-route-map-worker.ts +19 -10
  51. package/src/lib/route-map.ts +61 -16
  52. package/{bootstrap → template}/.agents/skills/remix/SKILL.md +162 -77
  53. package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
  54. package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
  55. package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +4 -1
  56. package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
  57. package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +42 -53
  58. package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
  59. package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
  60. package/{bootstrap → template}/AGENTS.md +10 -9
  61. package/template/README.md +29 -0
  62. package/template/app/actions/controller.tsx +18 -0
  63. package/template/app/assets/entry.ts +8 -0
  64. package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
  65. package/{bootstrap → template}/app/assets.ts +5 -3
  66. package/template/app/middleware/render.tsx +44 -0
  67. package/template/app/router.ts +20 -0
  68. package/{bootstrap → template}/app/routes.ts +1 -2
  69. package/{bootstrap → template}/app/ui/document.tsx +11 -4
  70. package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
  71. package/template/gitignore +4 -0
  72. package/{bootstrap → template}/package.json +5 -6
  73. package/template/public/favicon.svg +11 -0
  74. package/template/server.ts +38 -0
  75. package/{bootstrap → template}/tsconfig.json +3 -2
  76. package/bootstrap/README.md +0 -27
  77. package/bootstrap/app/assets/entry.ts +0 -19
  78. package/bootstrap/app/controllers/auth.tsx +0 -21
  79. package/bootstrap/app/controllers/home.tsx +0 -11
  80. package/bootstrap/app/router.ts +0 -16
  81. package/bootstrap/app/ui/layout.tsx +0 -22
  82. package/bootstrap/app/utils/render.tsx +0 -26
  83. package/bootstrap/server.ts +0 -37
  84. /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
  85. /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
  86. /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
  87. /package/{bootstrap → template}/.agents/skills/remix/references/mixins-styling-events.md +0 -0
@@ -68,18 +68,18 @@ fail fast when the secret is missing.
68
68
 
69
69
  ```typescript
70
70
  // Filesystem storage
71
- import { createFsSessionStorage } from 'remix/session/fs-storage'
71
+ import { createFsSessionStorage } from 'remix/session-storage/fs'
72
72
  export let sessionStorage = createFsSessionStorage('./tmp/sessions')
73
73
 
74
74
  // Memory storage (for tests)
75
- import { createMemorySessionStorage } from 'remix/session/memory-storage'
75
+ import { createMemorySessionStorage } from 'remix/session-storage/memory'
76
76
  export let sessionStorage = createMemorySessionStorage()
77
77
  ```
78
78
 
79
79
  ### Add session middleware
80
80
 
81
81
  ```typescript
82
- import { session } from 'remix/session-middleware'
82
+ import { session } from 'remix/middleware/session'
83
83
 
84
84
  let router = createRouter({
85
85
  middleware: [
@@ -151,7 +151,7 @@ bypassable by clearing cookies; if the guarantee needs to survive that, you also
151
151
  ### Basic setup
152
152
 
153
153
  ```typescript
154
- import { auth, createSessionAuthScheme } from 'remix/auth-middleware'
154
+ import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
155
155
  import { Session } from 'remix/session'
156
156
  import { Database } from 'remix/data-table'
157
157
 
@@ -179,7 +179,7 @@ export function loadAuth() {
179
179
  ### Reading auth state
180
180
 
181
181
  ```typescript
182
- import { Auth } from 'remix/auth-middleware'
182
+ import { Auth } from 'remix/middleware/auth'
183
183
 
184
184
  function handler({ get }) {
185
185
  let auth = get(Auth)
@@ -303,7 +303,9 @@ module-scope provider to `finishExternalAuth(...)` and `refreshExternalAuth(...)
303
303
  ### OAuth controller
304
304
 
305
305
  ```typescript
306
- export default {
306
+ import { createController } from 'remix/router'
307
+
308
+ export default createController(routes.auth.google, {
307
309
  actions: {
308
310
  // GET /auth/google — redirect to Google
309
311
  async index(context) {
@@ -326,10 +328,10 @@ export default {
326
328
  authAccountId: authAccount.id,
327
329
  })
328
330
 
329
- return redirect(returnTo ?? routes.account.href())
331
+ return redirect(returnTo ?? routes.account.index.href())
330
332
  },
331
333
  },
332
- } satisfies Controller<typeof routes.auth.google>
334
+ })
333
335
  ```
334
336
 
335
337
  ### Refresh stored provider tokens
@@ -356,20 +358,41 @@ async function refreshGoogleTokens({ get }) {
356
358
 
357
359
  ### Controller-level protection
358
360
 
359
- Apply `requireAuth()` to an entire controller subtree:
361
+ Apply `requireAuth()` to every action in one controller:
362
+
363
+ ```typescript
364
+ import { createController } from 'remix/router'
365
+ import { requireAuth } from 'remix/middleware/auth'
366
+
367
+ export default createController(routes.account, {
368
+ middleware: [requireAuth()],
369
+ actions: {
370
+ index() {
371
+ /* guaranteed authenticated */
372
+ },
373
+ },
374
+ })
375
+ ```
376
+
377
+ Nested route maps need their own explicit protection:
360
378
 
361
379
  ```typescript
362
- import { requireAuth } from 'remix/auth-middleware'
380
+ // app/router.ts
381
+ router.map(routes.account, accountController)
382
+ router.map(routes.account.settings, accountSettingsController)
363
383
 
364
- export default {
384
+ // app/actions/account/settings/controller.tsx
385
+ export default createController(routes.account.settings, {
365
386
  middleware: [requireAuth()],
366
387
  actions: {
367
388
  index() {
368
389
  /* guaranteed authenticated */
369
390
  },
370
- settings: settingsController,
391
+ update() {
392
+ /* guaranteed authenticated */
393
+ },
371
394
  },
372
- } satisfies Controller<typeof routes.account>
395
+ })
373
396
  ```
374
397
 
375
398
  ### Stacking middleware
@@ -377,14 +400,14 @@ export default {
377
400
  Combine auth checks with role checks:
378
401
 
379
402
  ```typescript
380
- export default {
403
+ export default createController(routes.admin, {
381
404
  middleware: [requireAuth(), requireAdmin()],
382
405
  actions: {
383
406
  index() {
384
407
  /* requires auth + admin */
385
408
  },
386
409
  },
387
- } satisfies Controller<typeof routes.admin>
410
+ })
388
411
  ```
389
412
 
390
413
  ### Action-level protection
@@ -392,9 +415,9 @@ export default {
392
415
  Apply middleware to a single route:
393
416
 
394
417
  ```typescript
395
- import { Auth, requireAuth } from 'remix/auth-middleware'
418
+ import { Auth, requireAuth } from 'remix/middleware/auth'
396
419
 
397
- router.get(routes.account, {
420
+ router.get(routes.account.index, {
398
421
  middleware: [requireAuth()],
399
422
  handler(context) {
400
423
  let auth = context.get(Auth)
@@ -406,7 +429,7 @@ router.get(routes.account, {
406
429
  ### Redirect on auth failure
407
430
 
408
431
  ```typescript
409
- import { requireAuth } from 'remix/auth-middleware'
432
+ import { requireAuth } from 'remix/middleware/auth'
410
433
  import { redirect } from 'remix/response/redirect'
411
434
 
412
435
  export function requireAuthRedirect() {
@@ -20,7 +20,10 @@ For host-element behavior (event handlers, styles, refs, animations), see
20
20
  A component has two phases:
21
21
 
22
22
  1. **Setup phase** — runs once when the component is created
23
- 2. **Render phase** — returned function runs on initial render and every update
23
+ 2. **Render phase** — returned zero-argument function runs on initial render and every update
24
+
25
+ The component shape is `function Component(handle: Handle<Props>) { return () => ... }`. Props are
26
+ available as `handle.props` in setup scope and are updated before every render.
24
27
 
25
28
  ```tsx
26
29
  import { on, type Handle } from 'remix/ui'
@@ -84,9 +84,16 @@ runtime only needs the column shape and validation hooks. Two valid patterns:
84
84
 
85
85
  Pick one and apply it consistently across the app.
86
86
 
87
- ### Table validation hooks
87
+ ### Table lifecycle hooks
88
88
 
89
- Tables can define `validate`, `beforeWrite`, and `afterRead` hooks:
89
+ Tables can define validation and lifecycle hooks:
90
+
91
+ - `validate` runs before `create` and `update` writes and should return either `{ value }` or
92
+ `{ issues }`
93
+ - `beforeWrite` can normalize or veto `create`/`update` values
94
+ - `afterWrite` observes completed `create`/`update` operations
95
+ - `beforeDelete` and `afterDelete` observe or veto deletes
96
+ - `afterRead` can normalize or reject row values after reads
90
97
 
91
98
  ```typescript
92
99
  export const books = table({
@@ -94,6 +101,12 @@ export const books = table({
94
101
  columns: {
95
102
  /* ... */
96
103
  },
104
+ beforeWrite({ value }) {
105
+ if (typeof value.slug === 'string') {
106
+ return { value: { ...value, slug: value.slug.trim().toLowerCase() } }
107
+ }
108
+ return { value }
109
+ },
97
110
  validate({ operation, value }) {
98
111
  let issues = []
99
112
  if (operation === 'create' && !value.slug) {
@@ -101,6 +114,9 @@ export const books = table({
101
114
  }
102
115
  return issues.length > 0 ? { issues } : { value }
103
116
  },
117
+ afterRead({ value }) {
118
+ return { value }
119
+ },
104
120
  })
105
121
  ```
106
122
 
@@ -111,7 +127,7 @@ Create a database with an adapter and expose it via middleware:
111
127
  ```typescript
112
128
  import BetterSqlite3 from 'better-sqlite3'
113
129
  import { createDatabase, Database } from 'remix/data-table'
114
- import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
130
+ import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
115
131
 
116
132
  let sqlite = new BetterSqlite3('./db/app.db')
117
133
  sqlite.pragma('foreign_keys = ON')
@@ -126,7 +142,7 @@ client fits the runtime instead of assuming `better-sqlite3` is required.
126
142
  ### Database middleware
127
143
 
128
144
  ```typescript
129
- import type { Middleware } from 'remix/fetch-router'
145
+ import type { Middleware } from 'remix/router'
130
146
  import { Database } from 'remix/data-table'
131
147
 
132
148
  export function loadDatabase(): Middleware {
@@ -284,7 +300,7 @@ The recommended way: register `formData()` middleware in the root stack and read
284
300
  the context system. This also lets `methodOverride()` and CSRF middleware work uniformly.
285
301
 
286
302
  ```typescript
287
- import { formData } from 'remix/form-data-middleware'
303
+ import { formData } from 'remix/middleware/form-data'
288
304
 
289
305
  let router = createRouter({
290
306
  middleware: [, /* ... */ formData() /* ... */],
@@ -9,7 +9,7 @@ involves:
9
9
  - Writing custom middleware that sets typed context values
10
10
  - Adding fast-exit handling (static files, CORS preflights) versus request-enriching layers
11
11
  (sessions, auth, data loading)
12
- - Booting a Node `http` server with `createRequestListener`
12
+ - Choosing when to keep the generated Node server versus switching server adapters
13
13
 
14
14
  For data and persistence specifics, see `data-and-validation.md`. For session and auth specifics,
15
15
  see `auth-and-sessions.md`.
@@ -22,14 +22,14 @@ request-enriching middleware (session, auth) later.
22
22
  Recommended ordering:
23
23
 
24
24
  ```typescript
25
- import { createRouter } from 'remix/fetch-router'
26
- import { compression } from 'remix/compression-middleware'
27
- import { formData } from 'remix/form-data-middleware'
28
- import { logger } from 'remix/logger-middleware'
29
- import { methodOverride } from 'remix/method-override-middleware'
30
- import { session } from 'remix/session-middleware'
31
- import { staticFiles } from 'remix/static-middleware'
32
- import { asyncContext } from 'remix/async-context-middleware'
25
+ import { createRouter } from 'remix/router'
26
+ import { compression } from 'remix/middleware/compression'
27
+ import { formData } from 'remix/middleware/form-data'
28
+ import { logger } from 'remix/middleware/logger'
29
+ import { methodOverride } from 'remix/middleware/method-override'
30
+ import { session } from 'remix/middleware/session'
31
+ import { staticFiles } from 'remix/middleware/static'
32
+ import { asyncContext } from 'remix/middleware/async-context'
33
33
 
34
34
  let middleware = []
35
35
 
@@ -53,18 +53,18 @@ let router = createRouter({ middleware })
53
53
 
54
54
  | Middleware | Import | Use when | Notes |
55
55
  | -------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
56
- | `staticFiles(dir, opts?)` | `remix/static-middleware` | Serve files from `public/` or another directory exactly as they exist on disk | Fast exit; usually near the top |
57
- | `compression()` | `remix/compression-middleware` | Compress text-like responses | Usually global |
58
- | `logger()` | `remix/logger-middleware` | Log requests and responses | Often development-only; `colors` can force color output on/off |
59
- | `cors(opts?)` | `remix/cors-middleware` | Endpoints must serve cross-origin browsers or preflight `OPTIONS` requests | Usually early so preflights can short-circuit |
60
- | `cop(opts?)` | `remix/cop-middleware` | Reject unsafe cross-origin browser requests without synchronizer tokens | Put before session or CSRF when used |
61
- | `formData(opts?)` | `remix/form-data-middleware` | Parse `FormData` bodies, especially forms and uploads | Needed for `_csrf` form field extraction |
62
- | `methodOverride()` | `remix/method-override-middleware` | HTML forms need `PUT`, `PATCH`, or `DELETE` semantics | Run after form parsing |
63
- | `session(cookie, storage)` | `remix/session-middleware` | Cookie-backed sessions | Must run before session-backed auth or CSRF |
64
- | `csrf(opts?)` | `remix/csrf-middleware` | Session-backed form workflows need synchronizer-token CSRF protection | Requires `session()` before it |
65
- | `asyncContext()` | `remix/async-context-middleware` | Helpers outside handlers need request context via `getContext()` | Add before helpers rely on it |
66
- | `auth({ schemes })` | `remix/auth-middleware` | Resolve auth state into `context.get(Auth)` | Run after `session()` for session-backed auth |
67
- | `requireAuth()` | `remix/auth-middleware` | A controller or action must reject anonymous access | Usually controller-level or action-level, not global |
56
+ | `staticFiles(dir, opts?)` | `remix/middleware/static` | Serve files from `public/` or another directory exactly as they exist on disk | Fast exit; usually near the top |
57
+ | `compression()` | `remix/middleware/compression` | Compress text-like responses | Usually global |
58
+ | `logger()` | `remix/middleware/logger` | Log requests and responses | Often development-only; `colors` can force color output on/off |
59
+ | `cors(opts?)` | `remix/middleware/cors` | Endpoints must serve cross-origin browsers or preflight `OPTIONS` requests | Usually early so preflights can short-circuit |
60
+ | `cop(opts?)` | `remix/middleware/cop` | Reject unsafe cross-origin browser requests without synchronizer tokens | Put before session or CSRF when used |
61
+ | `formData(opts?)` | `remix/middleware/form-data` | Parse `FormData` bodies, especially forms and uploads | Needed for `_csrf` form field extraction |
62
+ | `methodOverride()` | `remix/middleware/method-override` | HTML forms need `PUT`, `PATCH`, or `DELETE` semantics | Run after form parsing |
63
+ | `session(cookie, storage)` | `remix/middleware/session` | Cookie-backed sessions | Must run before session-backed auth or CSRF |
64
+ | `csrf(opts?)` | `remix/middleware/csrf` | Session-backed form workflows need synchronizer-token CSRF protection | Requires `session()` before it |
65
+ | `asyncContext()` | `remix/middleware/async-context` | Helpers outside handlers need request context via `getContext()` | Add before helpers rely on it |
66
+ | `auth({ schemes })` | `remix/middleware/auth` | Resolve auth state into `context.get(Auth)` | Run after `session()` for session-backed auth |
67
+ | `requireAuth()` | `remix/middleware/auth` | A controller or action must reject anonymous access | Usually controller-level or action-level, not global |
68
68
 
69
69
  ### Static files vs browser modules
70
70
 
@@ -103,7 +103,7 @@ staticFiles('./public', {
103
103
  })
104
104
 
105
105
  // Form data with upload handler
106
- import { FileUpload } from 'remix/form-data-parser'
106
+ import type { FileUpload } from 'remix/form-data-parser'
107
107
  import { createFsFileStorage } from 'remix/file-storage/fs'
108
108
 
109
109
  let fileStorage = createFsFileStorage('./tmp/uploads')
@@ -120,15 +120,16 @@ errors at the route boundary when they should become user-facing `Response` obje
120
120
 
121
121
  ## Writing Custom Middleware
122
122
 
123
- Middleware is a function that receives `(context, next)` and returns a `Response`. Call `next()` to
124
- continue the chain.
123
+ Middleware is a function that receives `(context, next)`. Return a `Response` to short-circuit, call
124
+ and return `next()` when you need the downstream response, or return nothing when you only set
125
+ context and want the router to continue automatically.
125
126
 
126
127
  ### Setting context values
127
128
 
128
129
  Use `context.set(key, value)` to add typed values accessible downstream via `context.get(key)`.
129
130
 
130
131
  ```typescript
131
- import type { Middleware } from 'remix/fetch-router'
132
+ import type { Middleware } from 'remix/router'
132
133
  import { Database } from 'remix/data-table'
133
134
 
134
135
  export function loadDatabase(): Middleware {
@@ -142,7 +143,7 @@ export function loadDatabase(): Middleware {
142
143
  ### Guarding routes
143
144
 
144
145
  ```typescript
145
- import { Auth } from 'remix/auth-middleware'
146
+ import { Auth } from 'remix/middleware/auth'
146
147
 
147
148
  export function requireAdmin(): Middleware {
148
149
  return (context, next) => {
@@ -163,8 +164,8 @@ helpers:
163
164
 
164
165
  ```typescript
165
166
  // app/utils/context.ts
166
- import { getContext } from 'remix/async-context-middleware'
167
- import { Auth } from 'remix/auth-middleware'
167
+ import { getContext } from 'remix/middleware/async-context'
168
+ import { Auth } from 'remix/middleware/auth'
168
169
  import { Database } from 'remix/data-table'
169
170
  import { Session } from 'remix/session'
170
171
 
@@ -200,18 +201,21 @@ Middleware can be applied at three levels:
200
201
  let router = createRouter({ middleware: [...] })
201
202
  ```
202
203
 
203
- 2. **Controller-level** — runs for all actions in a controller subtree:
204
+ 2. **Controller-level** — runs for the direct actions in one controller:
204
205
 
205
206
  ```typescript
206
- export default {
207
+ export default createController(routes.account, {
207
208
  middleware: [requireAuth()],
208
209
  actions: { ... },
209
- } satisfies Controller<typeof routes.account>
210
+ })
210
211
  ```
211
212
 
213
+ Controller middleware does not flow into other controllers. Add the middleware to each
214
+ controller that needs it.
215
+
212
216
  3. **Action-level** — runs for a single route:
213
217
  ```typescript
214
- router.get(routes.account, {
218
+ router.get(routes.account.index, {
215
219
  middleware: [requireAuth()],
216
220
  handler: accountAction.handler,
217
221
  })
@@ -219,25 +223,10 @@ Middleware can be applied at three levels:
219
223
 
220
224
  ## Node Server Setup
221
225
 
222
- Use `createRequestListener` to bridge Node's `http` module to the Fetch API router:
226
+ New apps already include a `server.ts` that adapts the app router with
227
+ `remix/node-fetch-server`. Keep that generated server unless the task specifically needs to change
228
+ runtime behavior such as host/protocol handling, TLS, HTTP/2, WebSockets, deployment lifecycle, or
229
+ test-only server setup.
223
230
 
224
- ```typescript
225
- import * as http from 'node:http'
226
- import { createRequestListener } from 'remix/node-fetch-server'
227
-
228
- let server = http.createServer(
229
- createRequestListener(async (request) => {
230
- try {
231
- return await router.fetch(request)
232
- } catch (error) {
233
- console.error(error)
234
- return new Response('Internal Server Error', { status: 500 })
235
- }
236
- }),
237
- )
238
-
239
- let port = Number(process.env.PORT) || 3000
240
- server.listen(port, () => {
241
- console.log(`http://localhost:${port}`)
242
- })
243
- ```
231
+ Use `remix/node-fetch-server` when you want to keep owning a standard Node `http`, `https`, or
232
+ `http2` server directly.
@@ -17,7 +17,7 @@ data is `data-and-validation.md`. For request lifecycle and middleware ordering,
17
17
 
18
18
  ## Route Builders
19
19
 
20
- Import all route builders from `remix/fetch-router/routes`.
20
+ Import all route builders from `remix/routes`.
21
21
 
22
22
  ### `route(prefix, map)` — nested route group
23
23
 
@@ -27,7 +27,7 @@ call (when you want a shared URL prefix) or a plain object literal (when each le
27
27
  its absolute path).
28
28
 
29
29
  ```typescript
30
- import { route, get, post } from 'remix/fetch-router/routes'
30
+ import { route, get, post } from 'remix/routes'
31
31
 
32
32
  export const routes = route({
33
33
  home: '/',
@@ -92,18 +92,22 @@ redirect(routes.account.orders.show.href({ orderId: '42' }))
92
92
 
93
93
  ## Actions
94
94
 
95
- An action is a handler for a single leaf route. Type it with `BuildAction`:
95
+ An action is the handler for one leaf route. In Remix app code, actions should live in controllers.
96
+ Use `Action` only when a reusable helper needs to type one action before it is added to a
97
+ controller or when you are doing low-level router wiring outside the `app/actions` convention:
96
98
 
97
99
  ```typescript
98
- import type { BuildAction } from 'remix/fetch-router'
100
+ import { createAction } from 'remix/router'
99
101
 
100
- export const search: BuildAction<'GET', typeof routes.search> = {
102
+ import { routes } from '../routes.ts'
103
+
104
+ export const search = createAction(routes.search, {
101
105
  async handler({ url }) {
102
106
  let query = url.searchParams.get('q') ?? ''
103
107
  let results = await searchIndex(query)
104
108
  return render(<SearchPage query={query} results={results} />)
105
109
  },
106
- }
110
+ })
107
111
  ```
108
112
 
109
113
  The handler receives a context object with:
@@ -116,9 +120,9 @@ The handler receives a context object with:
116
120
  Actions with inline middleware:
117
121
 
118
122
  ```typescript
119
- import { requireAuth } from 'remix/auth-middleware'
123
+ import { requireAuth } from 'remix/middleware/auth'
120
124
 
121
- router.get(routes.account, {
125
+ router.get(routes.account.index, {
122
126
  middleware: [requireAuth()],
123
127
  handler: accountAction.handler,
124
128
  })
@@ -195,17 +199,18 @@ if (!parsed.success) {
195
199
  ### Return JSON
196
200
 
197
201
  For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling
198
- APIs, inter-service calls), return a JSON `Response`. Use `remix/headers` for cache headers
199
- instead of hand-formatting strings:
202
+ APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when
203
+ typed header accessors make the response clearer:
200
204
 
201
205
  ```typescript
202
- import { CacheControl } from 'remix/headers'
206
+ import Headers from 'remix/headers'
207
+
208
+ let headers = new Headers()
209
+ headers.contentType = { mediaType: 'application/json', charset: 'utf-8' }
210
+ headers.cacheControl = { noStore: true }
203
211
 
204
212
  return new Response(JSON.stringify({ results }), {
205
- headers: {
206
- 'Content-Type': 'application/json; charset=utf-8',
207
- 'Cache-Control': new CacheControl({ noStore: true }).toString(),
208
- },
213
+ headers,
209
214
  })
210
215
  ```
211
216
 
@@ -216,15 +221,20 @@ with the server.
216
221
 
217
222
  ## Controllers
218
223
 
219
- A controller mirrors a route map. Each key in `actions` matches a key in the route definition.
220
- Pass `AppContext` as the second generic to `Controller` so `get(Database)`, `get(Session)`,
221
- `get(Auth)`, etc. are typed against your middleware stack.
224
+ A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct
225
+ leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not
226
+ belong inside a controller's `actions`; map those route maps with their own controllers.
227
+
228
+ Configure `RouterTypes.context` with your app context in the router module, then use
229
+ `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your
230
+ middleware stack without repeating a type clause on every controller.
222
231
 
223
232
  ```typescript
224
- import type { Controller } from 'remix/fetch-router'
225
- import type { AppContext } from '../router.ts'
233
+ import { createController } from 'remix/router'
234
+
235
+ import { routes } from '../routes.ts'
226
236
 
227
- export default {
237
+ export default createController(routes.books, {
228
238
  actions: {
229
239
  async index({ get }) {
230
240
  let db = get(Database)
@@ -239,74 +249,125 @@ export default {
239
249
  return render(<ShowPage book={book} />)
240
250
  },
241
251
  },
242
- } satisfies Controller<typeof routes.books, AppContext>
252
+ })
243
253
  ```
244
254
 
245
- ### Nested controllers
255
+ ### Root controller
246
256
 
247
- When a route map contains nested maps, the controller nests too:
257
+ The root route map uses `app/actions/controller.tsx` and owns only top-level leaf routes:
248
258
 
249
259
  ```typescript
250
260
  // routes.ts
251
261
  export const routes = route({
262
+ assets: get('/assets/*path'),
263
+ home: '/',
252
264
  account: route('account', {
253
265
  index: '/',
254
266
  settings: form('settings', { formMethod: 'PUT', names: { action: 'update' } }),
255
- orders: resources('orders', { only: ['index', 'show'], param: 'orderId' }),
256
267
  }),
257
268
  })
258
269
 
259
- // controllers/account/controller.tsx
260
- import settingsController from './settings/controller.tsx'
261
- import ordersController from './orders/controller.tsx'
270
+ // app/actions/controller.tsx
271
+ export default createController(routes, {
272
+ actions: {
273
+ async assets({ request }) {
274
+ return (await assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
275
+ },
276
+ home() {
277
+ return render(<HomePage />)
278
+ },
279
+ },
280
+ })
281
+ ```
282
+
283
+ Because `account` is a nested route map, it is not an action key in the root controller.
284
+
285
+ ### Nested route maps
286
+
287
+ Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`.
288
+ Directory names under `app/actions/` are route-map keys, not URL path segments.
262
289
 
263
- export default {
290
+ ```typescript
291
+ // app/actions/account/controller.tsx
292
+ export default createController(routes.account, {
264
293
  middleware: [requireAuth()],
265
294
  actions: {
266
- index() { return render(<AccountPage />) },
267
- settings: settingsController,
268
- orders: ordersController,
295
+ index() {
296
+ return render(<AccountPage />)
297
+ },
269
298
  },
270
- } satisfies Controller<typeof routes.account, AppContext>
299
+ })
300
+
301
+ // app/actions/account/settings/controller.tsx
302
+ export default createController(routes.account.settings, {
303
+ middleware: [requireAuth()],
304
+ actions: {
305
+ index() {
306
+ return render(<SettingsPage />)
307
+ },
308
+ update() {
309
+ return redirect(routes.account.index.href(), 303)
310
+ },
311
+ },
312
+ })
313
+ ```
314
+
315
+ Then map each route map explicitly:
316
+
317
+ ```typescript
318
+ import rootController from './actions/controller.tsx'
319
+ import accountController from './actions/account/controller.tsx'
320
+ import accountSettingsController from './actions/account/settings/controller.tsx'
321
+
322
+ let router = createRouter({ middleware })
323
+
324
+ router.map(routes, rootController)
325
+ router.map(routes.account, accountController)
326
+ router.map(routes.account.settings, accountSettingsController)
271
327
  ```
272
328
 
273
329
  ### Controller middleware
274
330
 
275
- The `middleware` array on a controller runs for every action in that subtree, before action-level
276
- middleware:
331
+ The `middleware` array on a controller runs only for the direct actions in that controller, before
332
+ action-level middleware. It does not apply to other controllers.
277
333
 
278
334
  ```typescript
279
- export default {
335
+ export default createController(routes.admin, {
280
336
  middleware: [requireAuth(), requireAdmin()],
281
337
  actions: {
282
338
  /* all actions require auth + admin */
283
339
  },
284
- } satisfies Controller<typeof routes.admin, AppContext>
340
+ })
285
341
  ```
286
342
 
287
343
  ## Registering Routes
288
344
 
289
- Use `router.map` for route maps (controllers) and verb methods for leaf routes:
345
+ Use `router.map` for route maps and controllers. Map each nested route map explicitly. Use verb
346
+ methods only for low-level router wiring outside the `app/actions` controller convention.
290
347
 
291
348
  ```typescript
292
349
  let router = createRouter({ middleware })
293
350
 
294
- // Route mapcontroller
351
+ // Route mapscontrollers
352
+ router.map(routes, rootController)
353
+ router.map(routes.contact, contactController)
295
354
  router.map(routes.auth, authController)
355
+ router.map(routes.auth.login, authLoginController)
296
356
  router.map(routes.admin, adminController)
357
+ router.map(routes.admin.books, adminBooksController)
297
358
 
298
- // Leaf route → action
299
- router.map(routes.home, home)
359
+ // Leaf route → one-off action
300
360
  router.get(routes.search, searchAction)
301
361
  router.post(routes.logout, logoutAction)
302
362
  ```
303
363
 
304
364
  ## Typed Context
305
365
 
306
- Define an `AppContext` type from your middleware stack for use in actions and controllers:
366
+ Define an `AppContext` type from your middleware stack, then make it the default context used by
367
+ `createAction()` and `createController()`:
307
368
 
308
369
  ```typescript
309
- import type { MiddlewareContext, WithParams, AnyParams } from 'remix/fetch-router'
370
+ import type { MiddlewareContext, ContextWithParams, AnyParams } from 'remix/router'
310
371
 
311
372
  type RootMiddleware = [
312
373
  ReturnType<typeof formData>,
@@ -315,10 +376,16 @@ type RootMiddleware = [
315
376
  ReturnType<typeof loadAuth>,
316
377
  ]
317
378
 
318
- export type AppContext<params extends AnyParams = AnyParams> = WithParams<
379
+ export type AppContext<params extends AnyParams = {}> = ContextWithParams<
319
380
  MiddlewareContext<RootMiddleware>,
320
381
  params
321
382
  >
383
+
384
+ declare module 'remix/router' {
385
+ interface RouterTypes {
386
+ context: AppContext
387
+ }
388
+ }
322
389
  ```
323
390
 
324
391
  This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.