@remix-run/cli 0.0.0 → 0.2.0

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -3
  3. package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
  4. package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
  5. package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
  6. package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
  7. package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
  8. package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
  9. package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
  10. package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  11. package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
  12. package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  13. package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
  14. package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
  15. package/bootstrap/AGENTS.md +39 -0
  16. package/bootstrap/README.md +27 -0
  17. package/bootstrap/app/assets/entry.ts +19 -0
  18. package/bootstrap/app/assets.ts +18 -0
  19. package/bootstrap/app/controllers/auth.tsx +21 -0
  20. package/bootstrap/app/controllers/home.tsx +11 -0
  21. package/bootstrap/app/router.ts +16 -0
  22. package/bootstrap/app/routes.ts +7 -0
  23. package/bootstrap/app/ui/document.tsx +26 -0
  24. package/bootstrap/app/ui/layout.tsx +22 -0
  25. package/bootstrap/app/ui/prompt-button.tsx +162 -0
  26. package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
  27. package/bootstrap/app/utils/render.tsx +26 -0
  28. package/bootstrap/package.json +22 -0
  29. package/bootstrap/server.ts +37 -0
  30. package/bootstrap/tsconfig.json +18 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/bootstrap-project.d.ts +17 -0
  35. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  36. package/dist/lib/bootstrap-project.js +137 -0
  37. package/dist/lib/cli-context.d.ts +11 -0
  38. package/dist/lib/cli-context.d.ts.map +1 -0
  39. package/dist/lib/cli-context.js +19 -0
  40. package/dist/lib/cli.d.ts +6 -0
  41. package/dist/lib/cli.d.ts.map +1 -0
  42. package/dist/lib/cli.js +84 -0
  43. package/dist/lib/commands/completion.d.ts +3 -0
  44. package/dist/lib/commands/completion.d.ts.map +1 -0
  45. package/dist/lib/commands/completion.js +51 -0
  46. package/dist/lib/commands/doctor.d.ts +4 -0
  47. package/dist/lib/commands/doctor.d.ts.map +1 -0
  48. package/dist/lib/commands/doctor.js +293 -0
  49. package/dist/lib/commands/help.d.ts +4 -0
  50. package/dist/lib/commands/help.d.ts.map +1 -0
  51. package/dist/lib/commands/help.js +100 -0
  52. package/dist/lib/commands/new.d.ts +4 -0
  53. package/dist/lib/commands/new.d.ts.map +1 -0
  54. package/dist/lib/commands/new.js +70 -0
  55. package/dist/lib/commands/routes.d.ts +4 -0
  56. package/dist/lib/commands/routes.d.ts.map +1 -0
  57. package/dist/lib/commands/routes.js +161 -0
  58. package/dist/lib/commands/test.d.ts +4 -0
  59. package/dist/lib/commands/test.d.ts.map +1 -0
  60. package/dist/lib/commands/test.js +23 -0
  61. package/dist/lib/commands/version.d.ts +4 -0
  62. package/dist/lib/commands/version.d.ts.map +1 -0
  63. package/dist/lib/commands/version.js +29 -0
  64. package/dist/lib/completion.d.ts +12 -0
  65. package/dist/lib/completion.d.ts.map +1 -0
  66. package/dist/lib/completion.js +310 -0
  67. package/dist/lib/contained-path.d.ts +2 -0
  68. package/dist/lib/contained-path.d.ts.map +1 -0
  69. package/dist/lib/contained-path.js +10 -0
  70. package/dist/lib/controller-files.d.ts +14 -0
  71. package/dist/lib/controller-files.d.ts.map +1 -0
  72. package/dist/lib/controller-files.js +53 -0
  73. package/dist/lib/controller-ownership.d.ts +41 -0
  74. package/dist/lib/controller-ownership.d.ts.map +1 -0
  75. package/dist/lib/controller-ownership.js +224 -0
  76. package/dist/lib/display-path.d.ts +2 -0
  77. package/dist/lib/display-path.d.ts.map +1 -0
  78. package/dist/lib/display-path.js +12 -0
  79. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  80. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  81. package/dist/lib/doctor/controller-findings.js +132 -0
  82. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  83. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  84. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  85. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  86. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  87. package/dist/lib/doctor/controller-placeholders.js +183 -0
  88. package/dist/lib/doctor/controllers.d.ts +8 -0
  89. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  90. package/dist/lib/doctor/controllers.js +13 -0
  91. package/dist/lib/doctor/environment.d.ts +17 -0
  92. package/dist/lib/doctor/environment.d.ts.map +1 -0
  93. package/dist/lib/doctor/environment.js +226 -0
  94. package/dist/lib/doctor/fixes.d.ts +3 -0
  95. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  96. package/dist/lib/doctor/fixes.js +65 -0
  97. package/dist/lib/doctor/project.d.ts +10 -0
  98. package/dist/lib/doctor/project.d.ts.map +1 -0
  99. package/dist/lib/doctor/project.js +371 -0
  100. package/dist/lib/doctor/types.d.ts +48 -0
  101. package/dist/lib/doctor/types.d.ts.map +1 -0
  102. package/dist/lib/doctor/types.js +15 -0
  103. package/dist/lib/errors.d.ts +167 -0
  104. package/dist/lib/errors.d.ts.map +1 -0
  105. package/dist/lib/errors.js +307 -0
  106. package/dist/lib/help-text.d.ts +13 -0
  107. package/dist/lib/help-text.d.ts.map +1 -0
  108. package/dist/lib/help-text.js +43 -0
  109. package/dist/lib/load-route-map-worker.d.ts +2 -0
  110. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  111. package/dist/lib/load-route-map-worker.js +92 -0
  112. package/dist/lib/parse-args.d.ts +22 -0
  113. package/dist/lib/parse-args.d.ts.map +1 -0
  114. package/dist/lib/parse-args.js +35 -0
  115. package/dist/lib/remix-version.d.ts +2 -0
  116. package/dist/lib/remix-version.d.ts.map +1 -0
  117. package/dist/lib/remix-version.js +58 -0
  118. package/dist/lib/reporter.d.ts +56 -0
  119. package/dist/lib/reporter.d.ts.map +1 -0
  120. package/dist/lib/reporter.js +357 -0
  121. package/dist/lib/route-map.d.ts +38 -0
  122. package/dist/lib/route-map.d.ts.map +1 -0
  123. package/dist/lib/route-map.js +195 -0
  124. package/dist/lib/terminal.d.ts +17 -0
  125. package/dist/lib/terminal.d.ts.map +1 -0
  126. package/dist/lib/terminal.js +66 -0
  127. package/package.json +47 -5
  128. package/src/index.ts +1 -0
  129. package/src/lib/bootstrap-project.ts +222 -0
  130. package/src/lib/cli-context.ts +38 -0
  131. package/src/lib/cli.ts +116 -0
  132. package/src/lib/commands/completion.ts +74 -0
  133. package/src/lib/commands/doctor.ts +393 -0
  134. package/src/lib/commands/help.ts +123 -0
  135. package/src/lib/commands/new.ts +104 -0
  136. package/src/lib/commands/routes.ts +251 -0
  137. package/src/lib/commands/test.ts +31 -0
  138. package/src/lib/commands/version.ts +39 -0
  139. package/src/lib/completion.ts +418 -0
  140. package/src/lib/contained-path.ts +13 -0
  141. package/src/lib/controller-files.ts +79 -0
  142. package/src/lib/controller-ownership.ts +397 -0
  143. package/src/lib/display-path.ts +16 -0
  144. package/src/lib/doctor/controller-findings.ts +157 -0
  145. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  146. package/src/lib/doctor/controller-placeholders.ts +242 -0
  147. package/src/lib/doctor/controllers.ts +23 -0
  148. package/src/lib/doctor/environment.ts +289 -0
  149. package/src/lib/doctor/fixes.ts +84 -0
  150. package/src/lib/doctor/project.ts +435 -0
  151. package/src/lib/doctor/types.ts +95 -0
  152. package/src/lib/errors.ts +367 -0
  153. package/src/lib/help-text.ts +88 -0
  154. package/src/lib/load-route-map-worker.ts +110 -0
  155. package/src/lib/parse-args.ts +82 -0
  156. package/src/lib/remix-version.ts +80 -0
  157. package/src/lib/reporter.ts +535 -0
  158. package/src/lib/route-map.ts +303 -0
  159. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,297 @@
1
+ # Hydration, Frames, and Navigation
2
+
3
+ ## What This Covers
4
+
5
+ How server-rendered UI becomes interactive in the browser, and how the page updates without a full
6
+ navigation. Read this when the task involves:
7
+
8
+ - Marking a component for client-side hydration with `clientEntry`
9
+ - Booting the client runtime with `run`
10
+ - Streaming server content into a region of the page with `<Frame>` and reloading those regions
11
+ - Triggering Navigation API transitions with `navigate(...)` or `link(...)`
12
+ - Server rendering with `renderToStream` or `renderToString`
13
+ - Managing the document `<head>`
14
+
15
+ For component-local state and updates, see `component-model.md`. For host-element behavior and
16
+ events, see `mixins-styling-events.md`.
17
+
18
+ ## Server First, Then Hydrate
19
+
20
+ Make the server route correct before adding `clientEntry(...)`. A POST should already do the right
21
+ thing on its own — return HTML, a redirect, or an error response — and a GET should already render
22
+ the page the user expects. `clientEntry` exists to layer interactivity on top of UI that already
23
+ works without it.
24
+
25
+ When server state changes after a mutation, prefer reloading a `<Frame>` when the UI region already
26
+ maps cleanly to a server-rendered route. Frames re-fetch the same route, so the rendering logic
27
+ stays in one place and the client does not need a parallel "state" API.
28
+
29
+ ```tsx
30
+ on('submit', async (event, signal) => {
31
+ event.preventDefault()
32
+ await fetch(routes.cart.add.href(), {
33
+ method: 'POST',
34
+ body: new FormData(event.currentTarget),
35
+ signal,
36
+ })
37
+ if (signal.aborted) return
38
+ await handle.frames.get('cart-summary')?.reload()
39
+ })
40
+ ```
41
+
42
+ Use polling or a small JSON state endpoint when the data changes outside this page, or when a tiny
43
+ shared widget would be heavier to model as a frame. Pick the lightest sync mechanism that preserves
44
+ clear ownership of rendering logic.
45
+
46
+ ## Client Entries
47
+
48
+ Use `clientEntry` to mark a component for client-side hydration. In source-served apps, prefer the
49
+ source module's `import.meta.url` as the entry ID and let server rendering map it to the public
50
+ asset URL:
51
+
52
+ ```tsx
53
+ import { clientEntry, on, type Handle } from 'remix/ui'
54
+
55
+ export const Counter = clientEntry(
56
+ import.meta.url,
57
+ function Counter(handle: Handle<{ initialCount: number; label: string }>) {
58
+ let count = handle.props.initialCount
59
+
60
+ return () => (
61
+ <div>
62
+ <span>
63
+ {handle.props.label}: {count}
64
+ </span>
65
+ <button
66
+ mix={on('click', () => {
67
+ count++
68
+ handle.update()
69
+ })}
70
+ >
71
+ +
72
+ </button>
73
+ </div>
74
+ )
75
+ },
76
+ )
77
+ ```
78
+
79
+ On the server, provide `resolveClientEntry` to `renderToStream(...)` so source file URLs become
80
+ browser-loadable asset URLs. Keep this resolution in the render helper so component modules do not
81
+ hard-code deployment-specific asset paths:
82
+
83
+ ```tsx
84
+ let stream = renderToStream(<App />, {
85
+ async resolveClientEntry(entryId, component) {
86
+ let exportName = entryId.split('#')[1] || component.name
87
+ if (!exportName) {
88
+ throw new Error(`Unable to resolve client entry export for ${entryId}`)
89
+ }
90
+
91
+ return {
92
+ href: await assetServer.getHref(entryId),
93
+ exportName,
94
+ }
95
+ },
96
+ })
97
+ ```
98
+
99
+ If the module export name differs from the component function name, include `#ExportName` in the
100
+ entry ID or return the exact export name from `resolveClientEntry`. A render helper that only
101
+ supports source-owned entries can also fail fast when `entryId` is not a `file://` URL.
102
+
103
+ On the server, `clientEntry` components render like any other component. The server wraps their
104
+ output in comment markers and serializes props into a `<script type="application/json">` tag.
105
+
106
+ Client entry props must be serializable: strings, numbers, booleans, `null`, `undefined`, plain
107
+ objects/arrays of the above, JSX elements, and `<Frame>` elements. Functions and class instances
108
+ cannot be passed.
109
+
110
+ ## Booting the Client
111
+
112
+ Use `run` to start the client runtime. It scans the document for client entry markers, loads
113
+ modules, and hydrates each one:
114
+
115
+ ```tsx
116
+ import { run } from 'remix/ui'
117
+
118
+ let app = run({
119
+ async loadModule(moduleUrl, exportName) {
120
+ let mod = await import(moduleUrl)
121
+ return mod[exportName]
122
+ },
123
+ async resolveFrame(src, signal, target) {
124
+ let headers = new Headers({ accept: 'text/html' })
125
+ if (target) headers.set('x-remix-target', target)
126
+ let response = await fetch(src, { headers, signal })
127
+ return response.body ?? (await response.text())
128
+ },
129
+ })
130
+
131
+ app.addEventListener('error', (event) => {
132
+ console.error('Component error:', event.error)
133
+ })
134
+
135
+ await app.ready()
136
+ ```
137
+
138
+ ### `run` options
139
+
140
+ - **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each
141
+ client entry. Typically uses dynamic `import()`.
142
+ - **`resolveFrame(src, signal, target)`** (optional) — called when a `<Frame>` loads or reloads
143
+ content. `target` is available when frame targeting matters.
144
+
145
+ ### `app` methods
146
+
147
+ - **`app.ready()`** — resolves when all initial client entries are hydrated
148
+ - **`app.flush()`** — synchronously flushes all pending updates
149
+ - **`app.dispose()`** — tears down all hydrated components
150
+
151
+ `app` is an `EventTarget` that emits `error` events from any hydrated component.
152
+
153
+ ## Frames
154
+
155
+ A `<Frame>` renders server content into the page. Frames stream after the initial HTML, nest inside
156
+ other frames, contain client entries, and can be reloaded without full page navigation.
157
+
158
+ ```tsx
159
+ import { Frame } from 'remix/ui'
160
+
161
+ function App() {
162
+ return () => (
163
+ <div>
164
+ <Frame src="/sidebar" fallback={<div>Loading...</div>} />
165
+ <Frame name="main" src="/main-content" />
166
+ </div>
167
+ )
168
+ }
169
+ ```
170
+
171
+ ### Frame props
172
+
173
+ - **`src`** (required) — URL to fetch the frame content from
174
+ - **`fallback`** (optional) — content to show while loading; determines streaming behavior
175
+ - **`name`** (optional) — registers the frame for lookup via `handle.frames.get(name)`
176
+ - **`on`** (optional) — event handlers for events dispatched from the frame element
177
+
178
+ ### Blocking vs non-blocking
179
+
180
+ - **Without `fallback`** (blocking) — the server waits for frame content before sending the initial
181
+ HTML chunk
182
+ - **With `fallback`** (non-blocking) — the fallback renders immediately; real content streams in
183
+ later and replaces it
184
+
185
+ ### Reloading frames
186
+
187
+ Client entries inside a frame can trigger a reload:
188
+
189
+ ```tsx
190
+ // Reload the containing frame
191
+ handle.frame.reload()
192
+
193
+ // Reload an adjacent named frame
194
+ await handle.frames.get('cart-summary')?.reload()
195
+
196
+ // Reload the entire page/frame tree
197
+ handle.frames.top.reload()
198
+ ```
199
+
200
+ When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props
201
+ while preserving their local component state.
202
+
203
+ ### Nested frames
204
+
205
+ Frames can nest. Each frame owns its own DOM region and hydrates client entries independently.
206
+ During SSR, `handle.frame.src` points at the frame being rendered, while
207
+ `handle.frames.top.src` stays fixed at the outer document URL.
208
+
209
+ ## Server Rendering
210
+
211
+ ### `renderToStream`
212
+
213
+ Renders a component tree to a `ReadableStream<Uint8Array>`. Sends initial HTML immediately and
214
+ streams frame content as it resolves:
215
+
216
+ ```tsx
217
+ import { renderToStream } from 'remix/ui/server'
218
+
219
+ let stream = renderToStream(<App />, {
220
+ frameSrc: request.url,
221
+ resolveFrame(src, target, context) {
222
+ let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
223
+ return fetchHtml(frameUrl)
224
+ },
225
+ onError(error) {
226
+ console.error(error)
227
+ },
228
+ })
229
+
230
+ return new Response(stream, {
231
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
232
+ })
233
+ ```
234
+
235
+ Options:
236
+
237
+ - **`frameSrc`** — seeds SSR frame state; populates `handle.frame.src` and `handle.frames.top.src`
238
+ - **`topFrameSrc`** — overrides the root frame URL for nested frame renders (carry forward from
239
+ `resolveFrame` context)
240
+ - **`resolveFrame(src, target, context)`** — return HTML string, `ReadableStream<Uint8Array>`, or a
241
+ promise of either. `context.currentFrameSrc` is the containing frame URL; `context.topFrameSrc`
242
+ is the outer document URL
243
+ - **`onError(error)`** — called on rendering errors
244
+
245
+ ### `renderToString`
246
+
247
+ Renders a component tree to a complete HTML string. Use for static pages or embedding HTML:
248
+
249
+ ```tsx
250
+ import { renderToString } from 'remix/ui/server'
251
+ let html = await renderToString(<App />)
252
+ ```
253
+
254
+ ### CSS in SSR
255
+
256
+ Components using the `css` mixin have styles collected during rendering and emitted as a single
257
+ `<style>` tag in `<head>`. No client-side style injection needed.
258
+
259
+ ## Navigation
260
+
261
+ Use real anchors for normal document navigation. For app-driven navigation:
262
+
263
+ - `navigate(href, options?)` — performs a Navigation API transition
264
+ - `link(href, options?)` mixin — makes any element behave like a navigation link
265
+
266
+ ```tsx
267
+ import { navigate } from 'remix/ui'
268
+ navigate('/dashboard', { history: 'replace' })
269
+ ```
270
+
271
+ Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
272
+
273
+ Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`.
274
+
275
+ ## Head Management
276
+
277
+ Manage document head with an explicit `<head>` in your document structure:
278
+
279
+ ```tsx
280
+ function App() {
281
+ return () => (
282
+ <html>
283
+ <head>
284
+ <title>Dashboard</title>
285
+ <meta name="description" content="Team dashboard" />
286
+ <link rel="stylesheet" href="/styles/app.css" />
287
+ </head>
288
+ <body>
289
+ <main>...</main>
290
+ </body>
291
+ </html>
292
+ )
293
+ }
294
+ ```
295
+
296
+ Put `title`, `meta`, `link`, and `style` tags inside an explicit `<head>`. Bare head-like tags
297
+ rendered outside `<head>` stay where they are — they are not moved into the document head for you.
@@ -0,0 +1,243 @@
1
+ # Middleware and Server Setup
2
+
3
+ ## What This Covers
4
+
5
+ How to compose the request lifecycle and bridge the router to a runtime. Read this when the task
6
+ involves:
7
+
8
+ - Choosing or ordering built-in middleware in the root stack
9
+ - Writing custom middleware that sets typed context values
10
+ - Adding fast-exit handling (static files, CORS preflights) versus request-enriching layers
11
+ (sessions, auth, data loading)
12
+ - Booting a Node `http` server with `createRequestListener`
13
+
14
+ For data and persistence specifics, see `data-and-validation.md`. For session and auth specifics,
15
+ see `auth-and-sessions.md`.
16
+
17
+ ## Middleware Stack
18
+
19
+ Middleware runs in order for every request. Place fast-exit middleware (static files) early and
20
+ request-enriching middleware (session, auth) later.
21
+
22
+ Recommended ordering:
23
+
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'
33
+
34
+ let middleware = []
35
+
36
+ if (process.env.NODE_ENV === 'development') {
37
+ middleware.push(logger())
38
+ }
39
+
40
+ middleware.push(compression())
41
+ middleware.push(staticFiles('./public'))
42
+ middleware.push(formData())
43
+ middleware.push(methodOverride())
44
+ middleware.push(session(cookie, storage))
45
+ middleware.push(asyncContext())
46
+ middleware.push(loadDatabase())
47
+ middleware.push(loadAuth())
48
+
49
+ let router = createRouter({ middleware })
50
+ ```
51
+
52
+ ### Built-in middleware catalog
53
+
54
+ | Middleware | Import | Use when | Notes |
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 |
68
+
69
+ ### Static files vs browser modules
70
+
71
+ - Use `staticFiles()` for files that should be served directly from disk, such as images, fonts,
72
+ or already-built assets in `public/`
73
+ - Use `remix/assets` when browser modules should be compiled and served from source files with
74
+ import rewriting, preloads, or fingerprinted URLs
75
+
76
+ ### Ordering notes
77
+
78
+ - Put fast exits early: `staticFiles()`, `cors()` preflight handling, and `cop()` when used
79
+ - Parse request bodies before middleware that depends on them, such as `methodOverride()` and form
80
+ field token extraction in `csrf()`
81
+ - Run `session()` before `csrf()` and before session-backed `auth()`
82
+ - Add `asyncContext()` before helpers or shared code call `getContext()`
83
+ - Keep route protection like `requireAuth()` at controller or action scope unless the entire app is
84
+ private
85
+
86
+ ### Common stacks
87
+
88
+ - **Session-backed HTML app** -> `compression()`, `staticFiles()`, optional `cop()`, `formData()`,
89
+ `methodOverride()`, `session()`, optional `csrf()`, `asyncContext()`, `auth({ schemes })`
90
+ - **Cross-origin API** -> `compression()`, `cors()`, optional `asyncContext()`, optional
91
+ `auth({ schemes })`
92
+ - **Upload flow** -> `compression()`, `staticFiles()`, `formData({ uploadHandler })`, then
93
+ sessions, auth, and data-loading middleware as needed
94
+
95
+ ### Middleware with options
96
+
97
+ ```typescript
98
+ // Static files with cache headers
99
+ staticFiles('./public', {
100
+ cacheControl: 'no-store, must-revalidate',
101
+ etag: false,
102
+ lastModified: false,
103
+ })
104
+
105
+ // Form data with upload handler
106
+ import { FileUpload } from 'remix/form-data-parser'
107
+ import { createFsFileStorage } from 'remix/file-storage/fs'
108
+
109
+ let fileStorage = createFsFileStorage('./tmp/uploads')
110
+
111
+ formData({
112
+ uploadHandler(fileUpload: FileUpload) {
113
+ return fileStorage.set(fileUpload.name, fileUpload)
114
+ },
115
+ })
116
+ ```
117
+
118
+ Errors thrown or rejected by `uploadHandler` propagate directly. Catch domain-specific upload
119
+ errors at the route boundary when they should become user-facing `Response` objects.
120
+
121
+ ## Writing Custom Middleware
122
+
123
+ Middleware is a function that receives `(context, next)` and returns a `Response`. Call `next()` to
124
+ continue the chain.
125
+
126
+ ### Setting context values
127
+
128
+ Use `context.set(key, value)` to add typed values accessible downstream via `context.get(key)`.
129
+
130
+ ```typescript
131
+ import type { Middleware } from 'remix/fetch-router'
132
+ import { Database } from 'remix/data-table'
133
+
134
+ export function loadDatabase(): Middleware {
135
+ return async (context, next) => {
136
+ context.set(Database, db)
137
+ return next()
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### Guarding routes
143
+
144
+ ```typescript
145
+ import { Auth } from 'remix/auth-middleware'
146
+
147
+ export function requireAdmin(): Middleware {
148
+ return (context, next) => {
149
+ let auth = context.get(Auth)
150
+ if (auth.identity?.role !== 'admin') {
151
+ return new Response('Forbidden', { status: 403 })
152
+ }
153
+ return next()
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### Async context for helpers
159
+
160
+ `asyncContext()` stores the request context in `AsyncLocalStorage` so helpers can reach it
161
+ without the context being threaded through every call. Wrap `getContext()` in app-specific
162
+ helpers:
163
+
164
+ ```typescript
165
+ // app/utils/context.ts
166
+ import { getContext } from 'remix/async-context-middleware'
167
+ import { Auth } from 'remix/auth-middleware'
168
+ import { Database } from 'remix/data-table'
169
+ import { Session } from 'remix/session'
170
+
171
+ export function getCurrentDb() {
172
+ return getContext().get(Database)
173
+ }
174
+
175
+ export function getCurrentSession() {
176
+ return getContext().get(Session)
177
+ }
178
+
179
+ export function getCurrentUser() {
180
+ let auth = getContext().get(Auth)
181
+ if (!auth.ok) {
182
+ throw new Error('Expected an authenticated user. Run requireAuth() before this code.')
183
+ }
184
+ return auth.identity
185
+ }
186
+
187
+ export function getCurrentUserSafely() {
188
+ let auth = getContext().get(Auth)
189
+ return auth.ok ? auth.identity : null
190
+ }
191
+ ```
192
+
193
+ ## Middleware Layers
194
+
195
+ Middleware can be applied at three levels:
196
+
197
+ 1. **Router-level** — runs for every request:
198
+
199
+ ```typescript
200
+ let router = createRouter({ middleware: [...] })
201
+ ```
202
+
203
+ 2. **Controller-level** — runs for all actions in a controller subtree:
204
+
205
+ ```typescript
206
+ export default {
207
+ middleware: [requireAuth()],
208
+ actions: { ... },
209
+ } satisfies Controller<typeof routes.account>
210
+ ```
211
+
212
+ 3. **Action-level** — runs for a single route:
213
+ ```typescript
214
+ router.get(routes.account, {
215
+ middleware: [requireAuth()],
216
+ handler: accountAction.handler,
217
+ })
218
+ ```
219
+
220
+ ## Node Server Setup
221
+
222
+ Use `createRequestListener` to bridge Node's `http` module to the Fetch API router:
223
+
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
+ ```