@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.
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +26 -4
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/completion.js +1 -5
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +5 -5
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +39 -4
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/completion.ts +1 -5
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/help-text.ts +3 -2
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/{bootstrap → template}/.agents/skills/remix/SKILL.md +162 -77
- package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
- package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
- package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +4 -1
- package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
- package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +42 -53
- package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
- package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
- package/{bootstrap → template}/AGENTS.md +10 -9
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
- package/{bootstrap → template}/app/assets.ts +5 -3
- package/template/app/middleware/render.tsx +44 -0
- package/template/app/router.ts +20 -0
- package/{bootstrap → template}/app/routes.ts +1 -2
- package/{bootstrap → template}/app/ui/document.tsx +11 -4
- package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/template/server.ts +38 -0
- package/{bootstrap → template}/tsconfig.json +3 -2
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/assets/entry.ts +0 -19
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -11
- package/bootstrap/app/router.ts +0 -16
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -26
- package/bootstrap/server.ts +0 -37
- /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
380
|
+
// app/router.ts
|
|
381
|
+
router.map(routes.account, accountController)
|
|
382
|
+
router.map(routes.account.settings, accountSettingsController)
|
|
363
383
|
|
|
364
|
-
|
|
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
|
-
|
|
391
|
+
update() {
|
|
392
|
+
/* guaranteed authenticated */
|
|
393
|
+
},
|
|
371
394
|
},
|
|
372
|
-
}
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
87
|
+
### Table lifecycle hooks
|
|
88
88
|
|
|
89
|
-
Tables can define
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
-
|
|
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/
|
|
26
|
-
import { compression } from 'remix/compression
|
|
27
|
-
import { formData } from 'remix/form-data
|
|
28
|
-
import { logger } from 'remix/logger
|
|
29
|
-
import { methodOverride } from 'remix/method-override
|
|
30
|
-
import { session } from 'remix/session
|
|
31
|
-
import { staticFiles } from 'remix/static
|
|
32
|
-
import { asyncContext } from 'remix/async-context
|
|
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
|
|
57
|
-
| `compression()` | `remix/compression
|
|
58
|
-
| `logger()` | `remix/logger
|
|
59
|
-
| `cors(opts?)` | `remix/cors
|
|
60
|
-
| `cop(opts?)` | `remix/cop
|
|
61
|
-
| `formData(opts?)` | `remix/form-data
|
|
62
|
-
| `methodOverride()` | `remix/method-override
|
|
63
|
-
| `session(cookie, storage)` | `remix/session
|
|
64
|
-
| `csrf(opts?)` | `remix/csrf
|
|
65
|
-
| `asyncContext()` | `remix/async-context
|
|
66
|
-
| `auth({ schemes })` | `remix/auth
|
|
67
|
-
| `requireAuth()` | `remix/auth
|
|
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)
|
|
124
|
-
|
|
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/
|
|
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
|
|
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
|
|
167
|
-
import { Auth } from 'remix/auth
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
100
|
+
import { createAction } from 'remix/router'
|
|
99
101
|
|
|
100
|
-
|
|
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
|
|
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`
|
|
199
|
-
|
|
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
|
|
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
|
|
220
|
-
|
|
221
|
-
`
|
|
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
|
|
225
|
-
|
|
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
|
-
}
|
|
252
|
+
})
|
|
243
253
|
```
|
|
244
254
|
|
|
245
|
-
###
|
|
255
|
+
### Root controller
|
|
246
256
|
|
|
247
|
-
|
|
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
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
290
|
+
```typescript
|
|
291
|
+
// app/actions/account/controller.tsx
|
|
292
|
+
export default createController(routes.account, {
|
|
264
293
|
middleware: [requireAuth()],
|
|
265
294
|
actions: {
|
|
266
|
-
index() {
|
|
267
|
-
|
|
268
|
-
|
|
295
|
+
index() {
|
|
296
|
+
return render(<AccountPage />)
|
|
297
|
+
},
|
|
269
298
|
},
|
|
270
|
-
}
|
|
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
|
|
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
|
-
}
|
|
340
|
+
})
|
|
285
341
|
```
|
|
286
342
|
|
|
287
343
|
## Registering Routes
|
|
288
344
|
|
|
289
|
-
Use `router.map` for route maps
|
|
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
|
|
351
|
+
// Route maps → controllers
|
|
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
|
|
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,
|
|
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 =
|
|
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.
|