@remix-run/cli 0.3.1 → 0.3.3

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.
@@ -2,8 +2,7 @@
2
2
 
3
3
  ## What This Covers
4
4
 
5
- Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when
6
- the task involves:
5
+ Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when the task involves:
7
6
 
8
7
  - Defining or changing the URL surface of the app
9
8
  - Writing or reorganizing controllers and actions
@@ -11,9 +10,7 @@ the task involves:
11
10
  - Returning a `Response` for HTML, redirects, JSON, or errors
12
11
  - Generating internal URLs with `.href()`
13
12
 
14
- The companion reference for shaping `Request` bodies, validating input, and dealing with persisted
15
- data is `data-and-validation.md`. For request lifecycle and middleware ordering, see
16
- `middleware-and-server.md`.
13
+ The companion reference for shaping `Request` bodies, validating input, and dealing with persisted data is `data-and-validation.md`. For request lifecycle and middleware ordering, see `middleware-and-server.md`.
17
14
 
18
15
  ## Route Builders
19
16
 
@@ -21,10 +18,7 @@ Import all route builders from `remix/routes`.
21
18
 
22
19
  ### `route(prefix, map)` — nested route group
23
20
 
24
- Adds a URL prefix to all children. Can also be called as `route(map)` without a prefix for a
25
- top-level grouping. Inside `route(...)`, a nested map may be either a `route('prefix', { ... })`
26
- call (when you want a shared URL prefix) or a plain object literal (when each leaf already owns
27
- its absolute path).
21
+ Adds a URL prefix to all children. Can also be called as `route(map)` without a prefix for a top-level grouping. Inside `route(...)`, a nested map may be either a `route('prefix', { ... })` call (when you want a shared URL prefix) or a plain object literal (when each leaf already owns its absolute path).
28
22
 
29
23
  ```typescript
30
24
  import { route, get, post } from 'remix/routes'
@@ -58,8 +52,7 @@ export const routes = route({
58
52
 
59
53
  ### `form(path, options?)` — form route
60
54
 
61
- Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action`
62
- (POST) by default.
55
+ Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action` (POST) by default.
63
56
 
64
57
  ```typescript
65
58
  contact: form('contact')
@@ -92,9 +85,7 @@ redirect(routes.account.orders.show.href({ orderId: '42' }))
92
85
 
93
86
  ## Actions
94
87
 
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:
88
+ An action is the handler for one leaf route. In Remix app code, actions should live in controllers. Use `Action` only when a reusable helper needs to type one action before it is added to a controller or when you are doing low-level router wiring outside the `app/actions` convention:
98
89
 
99
90
  ```typescript
100
91
  import { createAction } from 'remix/router'
@@ -117,21 +108,23 @@ The handler receives a context object with:
117
108
  - `url` — the request URL
118
109
  - `request` — the raw `Request`
119
110
 
120
- Actions with inline middleware:
111
+ Actions with action middleware:
121
112
 
122
113
  ```typescript
114
+ import { createAction } from 'remix/router'
123
115
  import { requireAuth } from 'remix/middleware/auth'
124
116
 
125
- router.get(routes.account.index, {
117
+ export const account = createAction(routes.account.index, {
126
118
  middleware: [requireAuth()],
127
- handler: accountAction.handler,
119
+ handler(context) {
120
+ return render(<AccountPage />)
121
+ },
128
122
  })
129
123
  ```
130
124
 
131
125
  ## Returning Responses
132
126
 
133
- An action returns a `Response`. The shape of that response is part of the route contract, and
134
- choosing it well saves a lot of glue elsewhere.
127
+ An action returns a `Response`. The shape of that response is part of the route contract, and choosing it well saves a lot of glue elsewhere.
135
128
 
136
129
  ### Render HTML
137
130
 
@@ -147,8 +140,7 @@ async handler({ get }) {
147
140
 
148
141
  ### Redirect after a mutation
149
142
 
150
- For state-changing routes (POST, PUT, PATCH, DELETE), the canonical reply is a redirect to the
151
- resulting page. Pass `303` explicitly when you want a POST-redirect-GET flow:
143
+ For state-changing routes (POST, PUT, PATCH, DELETE), the canonical reply is a redirect to the resulting page. Pass `303` explicitly when you want a POST-redirect-GET flow:
152
144
 
153
145
  ```typescript
154
146
  import { redirect } from 'remix/response/redirect'
@@ -167,13 +159,11 @@ async create({ get }) {
167
159
  }
168
160
  ```
169
161
 
170
- This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements
171
- on top.
162
+ This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements on top.
172
163
 
173
164
  ### Return an error response
174
165
 
175
- For expected failures — validation, conflict, not found — return a `Response` directly. Reserve
176
- thrown errors for genuinely unexpected failures.
166
+ For expected failures — validation, conflict, not found — return a `Response` directly. Reserve thrown errors for genuinely unexpected failures.
177
167
 
178
168
  ```typescript
179
169
  async show({ get, params }) {
@@ -198,9 +188,7 @@ if (!parsed.success) {
198
188
 
199
189
  ### Return JSON
200
190
 
201
- For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling
202
- APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when
203
- typed header accessors make the response clearer:
191
+ For routes consumed by client code rather than rendered as a page (autocomplete endpoints, polling APIs, inter-service calls), return a JSON `Response`. Use `SuperHeaders` from `remix/headers` when typed header accessors make the response clearer:
204
192
 
205
193
  ```typescript
206
194
  import Headers from 'remix/headers'
@@ -214,20 +202,13 @@ return new Response(JSON.stringify({ results }), {
214
202
  })
215
203
  ```
216
204
 
217
- If you find yourself returning JSON for what is really a browser form submission, prefer the
218
- redirect-after-POST pattern instead. JSON-only mutation endpoints make it harder to support
219
- non-JS clients, harder to share rendering logic, and easier for the client to drift out of sync
220
- with the server.
205
+ If you find yourself returning JSON for what is really a browser form submission, prefer the redirect-after-POST pattern instead. JSON-only mutation endpoints make it harder to support non-JS clients, harder to share rendering logic, and easier for the client to drift out of sync with the server.
221
206
 
222
207
  ## Controllers
223
208
 
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.
209
+ A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
227
210
 
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.
211
+ Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
231
212
 
232
213
  ```typescript
233
214
  import { createController } from 'remix/router'
@@ -284,8 +265,7 @@ Because `account` is a nested route map, it is not an action key in the root con
284
265
 
285
266
  ### Nested route maps
286
267
 
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.
268
+ Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`. Directory names under `app/actions/` are route-map keys, not URL path segments.
289
269
 
290
270
  ```typescript
291
271
  // app/actions/account/controller.tsx
@@ -328,8 +308,7 @@ router.map(routes.account.settings, accountSettingsController)
328
308
 
329
309
  ### Controller middleware
330
310
 
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.
311
+ The `middleware` array on a controller runs only for the direct actions in that controller, before action middleware. It does not apply to other controllers.
333
312
 
334
313
  ```typescript
335
314
  export default createController(routes.admin, {
@@ -342,8 +321,7 @@ export default createController(routes.admin, {
342
321
 
343
322
  ## Registering Routes
344
323
 
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.
324
+ Use `router.map` for route maps and controllers. Map each nested route map explicitly. Use verb methods only for low-level router wiring outside the `app/actions` controller convention.
347
325
 
348
326
  ```typescript
349
327
  let router = createRouter({ middleware })
@@ -363,23 +341,16 @@ router.post(routes.logout, logoutAction)
363
341
 
364
342
  ## Typed Context
365
343
 
366
- Define an `AppContext` type from your middleware stack, then make it the default context used by
367
- `createAction()` and `createController()`:
344
+ Define an `AppContext` type from your router, then make it the default context used by `createAction()` and `createController()`:
368
345
 
369
346
  ```typescript
370
- import type { MiddlewareContext, ContextWithParams, AnyParams } from 'remix/router'
371
-
372
- type RootMiddleware = [
373
- ReturnType<typeof formData>,
374
- ReturnType<typeof session>,
375
- ReturnType<typeof loadDatabase>,
376
- ReturnType<typeof loadAuth>,
377
- ]
378
-
379
- export type AppContext<params extends AnyParams = {}> = ContextWithParams<
380
- MiddlewareContext<RootMiddleware>,
381
- params
382
- >
347
+ import { createRouter, type RouterContext } from 'remix/router'
348
+
349
+ export const router = createRouter({
350
+ middleware: [formData(), session(cookie, storage), loadDatabase(), loadAuth()],
351
+ })
352
+
353
+ export type AppContext = RouterContext<typeof router>
383
354
 
384
355
  declare module 'remix/router' {
385
356
  interface RouterTypes {
@@ -2,8 +2,7 @@
2
2
 
3
3
  ## What This Covers
4
4
 
5
- How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when
6
- the task involves:
5
+ How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when the task involves:
7
6
 
8
7
  - Driving the router with `router.fetch(new Request(...))` and asserting on the returned `Response`
9
8
  - Building a fresh router per test for session, storage, or database isolation
@@ -12,24 +11,18 @@ the task involves:
12
11
  - Using adjacent CLI checks such as `remix routes`, `remix doctor`, and `remix version`
13
12
  - Choosing which layer to test for a given behavior
14
13
 
15
- For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see
16
- `component-model.md`.
14
+ For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see `component-model.md`.
17
15
 
18
16
  ## Two Shapes
19
17
 
20
- Remix tests run with `remix test`, use `remix/test` for the test framework, and use
21
- `remix/assert` for assertions. Two main shapes:
18
+ Remix tests run with `remix test`, use `remix/test` for the test framework, and use `remix/assert` for assertions. Two main shapes:
22
19
 
23
- - **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert
24
- on the returned `Response`. No DOM, no browser harness.
25
- - **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use
26
- `createRoot(...)` directly when you need lower-level root control.
20
+ - **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert on the returned `Response`. No DOM, no browser harness.
21
+ - **Component tests** render a component into a real DOM `Element` with `render(...)`, or use `createRoot(...)` directly when you need lower-level root control.
27
22
 
28
23
  ## Server / Router Tests
29
24
 
30
- Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router
31
- per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays
32
- isolated.
25
+ Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays isolated.
33
26
 
34
27
  ```ts
35
28
  import * as assert from 'remix/assert'
@@ -49,10 +42,7 @@ describe('home', () => {
49
42
  })
50
43
  ```
51
44
 
52
- Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route
53
- definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need
54
- a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the
55
- router.
45
+ Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the router.
56
46
 
57
47
  ```ts
58
48
  import { createMemorySessionStorage } from 'remix/session-storage/memory'
@@ -64,8 +54,7 @@ let router = createBookstoreRouter({
64
54
  })
65
55
  ```
66
56
 
67
- Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real
68
- HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
57
+ Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
69
58
 
70
59
  ```ts
71
60
  import { createTestServer } from 'remix/node-fetch-server/test'
@@ -102,16 +91,11 @@ export default {
102
91
  }
103
92
  ```
104
93
 
105
- Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery
106
- would otherwise enter generated output, symlinked workspaces, or other paths that should not
107
- produce tests.
94
+ Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests.
108
95
 
109
96
  ## Component Tests
110
97
 
111
- Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container,
112
- flushes the initial render, and returns `act(...)` so interactions can flush pending updates before
113
- assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control
114
- over root rendering, flushing, or disposal.
98
+ Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container, flushes the initial render, and returns `act(...)` so interactions can flush pending updates before assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control over root rendering, flushing, or disposal.
115
99
 
116
100
  ### Basic pattern
117
101
 
@@ -130,8 +114,7 @@ result.cleanup()
130
114
 
131
115
  ### Why act / flush
132
116
 
133
- - **After initial render** — ensures event listeners are attached and the DOM is ready for
134
- interaction.
117
+ - **After initial render** — ensures event listeners are attached and the DOM is ready for interaction.
135
118
  - **After interactions** — applies updates from `handle.update()` calls triggered by events.
136
119
  - **After async work resolves** — applies updates from resolved `queueTask(...)` callbacks.
137
120
 
@@ -152,8 +135,7 @@ assert.equal(result.container.textContent, 'Expected data')
152
135
 
153
136
  ### Component removal
154
137
 
155
- Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup
156
- behavior:
138
+ Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup behavior:
157
139
 
158
140
  ```tsx
159
141
  let result = render(<MyComponent />)
@@ -168,5 +150,4 @@ assert.throws(() => result.$('.content'), /cleaned up/)
168
150
 
169
151
  - Prefer real DOM interactions over mocking framework behavior.
170
152
  - Avoid testing implementation-only markers unless they are the only stable synchronization point.
171
- - One representative flow proving a behavior is better than repeating the same assertion across many
172
- paths.
153
+ - One representative flow proving a behavior is better than repeating the same assertion across many paths.
@@ -1,4 +1,5 @@
1
- import { css, type RemixNode } from 'remix/ui'
1
+ import type { Handle, RemixNode } from 'remix/ui'
2
+ import { css } from 'remix/ui'
2
3
 
3
4
  import { routes } from '../routes.ts'
4
5
 
@@ -10,22 +11,26 @@ export interface DocumentProps {
10
11
 
11
12
  const DEFAULT_TITLE = readAppDisplayName('%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%')
12
13
 
13
- export function Document() {
14
- return ({ children, head, title = DEFAULT_TITLE }: DocumentProps) => (
15
- <html lang="en">
16
- <head>
17
- <meta charSet="utf-8" />
18
- <meta name="viewport" content="width=device-width, initial-scale=1" />
19
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
20
- <title>{title}</title>
21
- {head}
22
- </head>
23
- <body mix={css({ margin: 0 })}>
24
- {children}
25
- <script type="module" src={routes.assets.href({ path: 'app/assets/entry.ts' })}></script>
26
- </body>
27
- </html>
28
- )
14
+ export function Document(handle: Handle<DocumentProps>) {
15
+ return () => {
16
+ let { children, head, title = DEFAULT_TITLE } = handle.props
17
+
18
+ return (
19
+ <html lang="en">
20
+ <head>
21
+ <meta charSet="utf-8" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
24
+ <title>{title}</title>
25
+ {head}
26
+ </head>
27
+ <body mix={css({ margin: 0 })}>
28
+ {children}
29
+ <script type="module" src={routes.assets.href({ path: 'app/assets/entry.ts' })}></script>
30
+ </body>
31
+ </html>
32
+ )
33
+ }
29
34
  }
30
35
 
31
36
  function readAppDisplayName(value: string): string {
@@ -1,5 +1,6 @@
1
1
  // Delete this file and put your own home page in app/actions/controller.tsx
2
- import { css, type RemixNode } from 'remix/ui'
2
+ import type { Handle, RemixNode } from 'remix/ui'
3
+ import { css } from 'remix/ui'
3
4
 
4
5
  import { PromptButton } from '../assets/prompt-button.tsx'
5
6
  import { Document } from './document.tsx'
@@ -203,54 +204,62 @@ function CodingWithAiCard() {
203
204
  )
204
205
  }
205
206
 
206
- function CardLink() {
207
- return ({ href, icon, label }: { href: string; icon: RemixNode; label: string }) => (
208
- <a
209
- href={href}
210
- mix={css({
211
- display: 'flex',
212
- gap: '16px',
213
- alignItems: 'center',
214
- padding: '16px',
215
- borderRadius: '12px',
216
- color: 'var(--text-primary)',
217
- textDecoration: 'none',
218
- background: 'transparent',
219
- transition: 'background-color 150ms ease, color 150ms ease',
220
- '&:hover, &:focus-visible': {
221
- background: 'var(--surface-4)',
222
- color: 'var(--brand-blue)',
223
- outline: 'none',
224
- },
225
- })}
226
- >
227
- <IconSlot>{icon}</IconSlot>
228
- <span mix={css({ fontSize: '14px', lineHeight: 1.5, whiteSpace: 'nowrap' })}>{label}</span>
229
- </a>
230
- )
207
+ function CardLink(handle: Handle<{ href: string; icon: RemixNode; label: string }>) {
208
+ return () => {
209
+ let { href, icon, label } = handle.props
210
+
211
+ return (
212
+ <a
213
+ href={href}
214
+ mix={css({
215
+ display: 'flex',
216
+ gap: '16px',
217
+ alignItems: 'center',
218
+ padding: '16px',
219
+ borderRadius: '12px',
220
+ color: 'var(--text-primary)',
221
+ textDecoration: 'none',
222
+ background: 'transparent',
223
+ transition: 'background-color 150ms ease, color 150ms ease',
224
+ '&:hover, &:focus-visible': {
225
+ background: 'var(--surface-4)',
226
+ color: 'var(--brand-blue)',
227
+ outline: 'none',
228
+ },
229
+ })}
230
+ >
231
+ <IconSlot>{icon}</IconSlot>
232
+ <span mix={css({ fontSize: '14px', lineHeight: 1.5, whiteSpace: 'nowrap' })}>{label}</span>
233
+ </a>
234
+ )
235
+ }
231
236
  }
232
237
 
233
- function IconSlot() {
234
- return ({ children, rotated = false }: { children: RemixNode; rotated?: boolean }) => (
235
- <span
236
- aria-hidden="true"
237
- mix={css({
238
- flex: '0 0 24px',
239
- width: '24px',
240
- display: 'flex',
241
- alignItems: 'center',
242
- justifyContent: rotated ? 'center' : 'flex-start',
243
- '& svg': {
244
- width: '20px',
245
- height: '20px',
246
- display: 'block',
247
- ...(rotated ? { transform: 'rotate(180deg)' } : {}),
248
- },
249
- })}
250
- >
251
- {children}
252
- </span>
253
- )
238
+ function IconSlot(handle: Handle<{ children: RemixNode; rotated?: boolean }>) {
239
+ return () => {
240
+ let { children, rotated = false } = handle.props
241
+
242
+ return (
243
+ <span
244
+ aria-hidden="true"
245
+ mix={css({
246
+ flex: '0 0 24px',
247
+ width: '24px',
248
+ display: 'flex',
249
+ alignItems: 'center',
250
+ justifyContent: rotated ? 'center' : 'flex-start',
251
+ '& svg': {
252
+ width: '20px',
253
+ height: '20px',
254
+ display: 'block',
255
+ ...(rotated ? { transform: 'rotate(180deg)' } : {}),
256
+ },
257
+ })}
258
+ >
259
+ {children}
260
+ </span>
261
+ )
262
+ }
254
263
  }
255
264
 
256
265
  function Footer() {