@remix-run/cli 0.2.0 → 0.3.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.
- 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/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 +3 -3
- 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/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/{bootstrap → template}/.agents/skills/remix/SKILL.md +160 -73
- 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/data-and-validation.md +21 -5
- package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +43 -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 +43 -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/{bootstrap → template}/server.ts +10 -11
- 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 → template}/.agents/skills/remix/references/animate-elements.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/component-model.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
|
@@ -5,9 +5,22 @@ description: Build and review Remix 3 applications using the `remix` npm package
|
|
|
5
5
|
|
|
6
6
|
# Build a Remix App
|
|
7
7
|
|
|
8
|
-
Use this skill for end-to-end Remix app work.
|
|
8
|
+
Use this skill for end-to-end Remix app work. This skill helps you choose the right layer
|
|
9
9
|
first, reach for the right package, and avoid the most common Remix-specific mistakes.
|
|
10
10
|
|
|
11
|
+
## Full Package Documentation
|
|
12
|
+
|
|
13
|
+
This skill is the quick guide. When you need fuller API documentation, examples, or package-specific
|
|
14
|
+
details for a `remix/*` subpath, first look for a README next to the relevant generated source file
|
|
15
|
+
in the published `remix` package:
|
|
16
|
+
`node_modules/remix/src/<subpath>/README.md`. If that README does not exist, look for the nearest
|
|
17
|
+
parent README because some subpaths share their parent package documentation.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
|
|
21
|
+
- `remix/router` -> `node_modules/remix/src/fetch-router/README.md`
|
|
22
|
+
- `remix/ui/button` -> `node_modules/remix/src/ui/button/README.md`
|
|
23
|
+
|
|
11
24
|
## What Remix Is
|
|
12
25
|
|
|
13
26
|
Remix 3 is a server-first web framework built on Web APIs such as `Request`, `Response`, `URL`,
|
|
@@ -17,7 +30,7 @@ subpath. There is no top-level `remix` import.
|
|
|
17
30
|
A Remix app has four main pieces:
|
|
18
31
|
|
|
19
32
|
- **Routes** in `app/routes.ts` define the typed URL contract and power `href()` generation.
|
|
20
|
-
- **Controllers
|
|
33
|
+
- **Controllers** in `app/actions` implement that contract and return `Response` objects.
|
|
21
34
|
- **Middleware** composes request lifecycle behavior and populates typed context via
|
|
22
35
|
`context.set(Key, value)`.
|
|
23
36
|
- **Components** render UI with `remix/ui`. This is not React. A component receives a
|
|
@@ -101,7 +114,8 @@ Use these root directories consistently:
|
|
|
101
114
|
Inside `app/`, organize by responsibility:
|
|
102
115
|
|
|
103
116
|
- `assets/` for client entrypoints and client-owned browser behavior
|
|
104
|
-
- `
|
|
117
|
+
- `actions/` for controller-owned route handlers, route-local response rendering, and route-local
|
|
118
|
+
UI/helpers that are not shared across route areas
|
|
105
119
|
- `data/` for schema, queries, persistence setup, migrations, and runtime data initialization
|
|
106
120
|
- `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database
|
|
107
121
|
injection
|
|
@@ -123,31 +137,56 @@ When code could live in multiple places:
|
|
|
123
137
|
|
|
124
138
|
### Route Ownership
|
|
125
139
|
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
`
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
140
|
+
- Put top-level leaf actions in `app/actions/controller.tsx`
|
|
141
|
+
- A controller's `actions` object contains only direct leaf route keys from the route map passed to
|
|
142
|
+
`router.map(...)`
|
|
143
|
+
- Add `app/actions/<route-key>/controller.tsx` for each nested route map that needs actions or
|
|
144
|
+
middleware, and map it explicitly with `router.map(routes.<routeKey>, controller)`
|
|
145
|
+
- Name directories under `app/actions/` after route-map keys, not URL path segments
|
|
146
|
+
- Keep route-local UI and helpers next to the controller that owns them
|
|
147
|
+
- Move shared cross-route UI to `app/ui/`
|
|
148
|
+
- If a top-level leaf grows into a route map, move its handler into the nested route-key
|
|
149
|
+
controller and update `app/router.ts` to map that route map explicitly
|
|
150
|
+
|
|
151
|
+
### Response Rendering And Utilities
|
|
152
|
+
|
|
153
|
+
- Treat response rendering as action-layer code: modules that return `Response`, choose HTTP status
|
|
154
|
+
or headers, call `redirect(...)`, or call the local `render(...)` helper belong in `app/actions`
|
|
155
|
+
- Keep `app/actions/render.tsx` small; it should adapt `remix/ui/server` output to
|
|
156
|
+
`createHtmlResponse(...)`. Route-specific response assembly can live in flat action modules, but
|
|
157
|
+
directories under `app/actions/` must still match route-map keys
|
|
158
|
+
- Put pure support code in focused `app/utils/<topic>.ts` modules. Formatting, MIME
|
|
159
|
+
classification, path parsing, sorting, and normalization should be testable without a router,
|
|
160
|
+
request context, or `Response`, and should not import from `app/actions`, `remix/ui/server`, or
|
|
161
|
+
`remix/response/*`
|
|
162
|
+
- Do not introduce page-data intermediary shapes only to keep route-specific renderers away from
|
|
163
|
+
`render(...)`; keep response assembly in actions and extract only the pure helpers
|
|
133
164
|
|
|
134
165
|
### Layout Anti-Patterns
|
|
135
166
|
|
|
136
167
|
- Do not create `app/lib/` as a generic dumping ground
|
|
137
168
|
- Do not create `app/components/` as a second shared UI bucket when `app/ui/` already owns that
|
|
138
169
|
role
|
|
139
|
-
- Do not
|
|
170
|
+
- Do not create `app/controllers/`; Remix app route handlers live under `app/actions/`
|
|
171
|
+
- Do not put shared cross-route UI in `app/actions/`
|
|
172
|
+
- Do not create standalone root action files; put root route actions in `app/actions/controller.tsx`
|
|
173
|
+
- Do not put nested route-map keys in a controller's `actions`
|
|
174
|
+
- Do not register normal app leaf routes directly in `app/router.ts` when they belong in a
|
|
175
|
+
controller
|
|
176
|
+
- Do not rely on middleware from one controller to protect another controller; map middleware
|
|
177
|
+
explicitly in each controller that needs it
|
|
140
178
|
- Do not put middleware or persistence helpers in `app/utils/` when they have a clearer home
|
|
141
|
-
- Do not create folders for simple leaf actions unless they are real controllers
|
|
142
179
|
|
|
143
180
|
## Core Remix Rules
|
|
144
181
|
|
|
145
182
|
- Import from `remix/<subpath>`, never `import { ... } from 'remix'`
|
|
146
183
|
- Treat `app/routes.ts` as the source of truth for URLs. Use `routes.<name>.href(...)` for
|
|
147
184
|
redirects, links, tests, and internal URL construction
|
|
148
|
-
- Controllers
|
|
185
|
+
- Controllers should return explicit `Response` objects, including redirects, 404s, and
|
|
149
186
|
validation failures. At the route boundary, prefer returning a `Response` for expected outcomes
|
|
150
187
|
(validation errors, conflicts, not found) over throwing for control flow
|
|
188
|
+
- `router.map(routes, controller)` maps only the direct leaf routes in `routes`; nested route maps
|
|
189
|
+
must be mapped with their own explicit controllers
|
|
151
190
|
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types
|
|
152
191
|
are part of the route contract
|
|
153
192
|
- Make the server route correct first. A POST should already return the right HTML, redirect, or
|
|
@@ -185,11 +224,16 @@ When code could live in multiple places:
|
|
|
185
224
|
|
|
186
225
|
- Prefer server and router tests first. Drive the app with `router.fetch(new Request(...))` and
|
|
187
226
|
assert on the returned `Response`
|
|
227
|
+
- Keep controller tests shaped like controllers: root route behavior belongs in
|
|
228
|
+
`app/actions/controller.test.ts(x)`, and nested route-map behavior belongs beside that route-key
|
|
229
|
+
controller
|
|
188
230
|
- Build a fresh router per test or per suite so sessions, in-memory storage, and database state
|
|
189
231
|
stay isolated
|
|
190
232
|
- Use `routes.<name>.href(...)` in tests so URLs stay coupled to the route contract
|
|
191
233
|
- For auth or session scenarios, use a test cookie and `createMemorySessionStorage()` instead of
|
|
192
234
|
production storage
|
|
235
|
+
- Co-locate tests for pure `app/utils` helpers beside their modules. Test response behavior through
|
|
236
|
+
router or controller tests
|
|
193
237
|
- Use component tests only for interactive or DOM-specific behavior. Render with `createRoot(...)`,
|
|
194
238
|
interact with the real DOM, and call `root.flush()` between steps
|
|
195
239
|
- Prefer one representative behavior test over many repetitive assertion variants
|
|
@@ -216,6 +260,13 @@ When code could live in multiple places:
|
|
|
216
260
|
- Assuming authentication is enough without per-resource authorization checks
|
|
217
261
|
- Dropping shared code into vague buckets like `utils.ts`, `helpers.ts`, or `common.ts` when
|
|
218
262
|
ownership is known
|
|
263
|
+
- Recreating the old `app/controllers` or standalone root action file layout instead of using
|
|
264
|
+
controllers under `app/actions`
|
|
265
|
+
- Putting nested route-map keys inside a controller `actions` object. Map nested route maps
|
|
266
|
+
explicitly in `app/router.ts`
|
|
267
|
+
- Treating direct `router.get(...)`/`router.post(...)` registrations as the default app structure
|
|
268
|
+
instead of using controllers
|
|
269
|
+
- Assuming controller middleware applies to controllers registered for nested route maps
|
|
219
270
|
- Writing only component tests for a feature whose main behavior is really an HTTP route concern
|
|
220
271
|
|
|
221
272
|
## Package Map
|
|
@@ -225,17 +276,22 @@ what it exports. Open the linked reference file when you need full examples.
|
|
|
225
276
|
|
|
226
277
|
### Routing, Server, and Responses
|
|
227
278
|
|
|
228
|
-
- `remix/
|
|
279
|
+
- `remix/router` — the router itself. Use for `createRouter`, controller and middleware
|
|
229
280
|
types, and registering routes
|
|
230
|
-
- `remix/
|
|
231
|
-
`
|
|
232
|
-
- `remix/node-fetch-server` —
|
|
233
|
-
`
|
|
281
|
+
- `remix/routes` — declarative route builders. Use for `route`, `get`, `post`, `put`, `del`,
|
|
282
|
+
`form`, `resources` when defining `app/routes.ts`
|
|
283
|
+
- `remix/node-fetch-server` — default Node adapter for new apps. Use `createRequestListener` with
|
|
284
|
+
`node:http`, `node:https`, or `node:http2` in `server.ts` when booting the template-style app
|
|
285
|
+
- `remix/node-serve` — managed high-performance Node server. Use `serve` when you want Remix to
|
|
286
|
+
manage the server lifecycle, TLS options, or uWebSockets.js setup around a Fetch handler
|
|
234
287
|
- `remix/assets` — browser asset server. Use for `createAssetServer` when serving compiled
|
|
235
|
-
scripts and styles, getting public hrefs, and emitting preloads.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
288
|
+
scripts and styles, getting public hrefs, and emitting preloads. Configure a `basePath`, and
|
|
289
|
+
keep `fileMap` URL patterns relative to it. Shared compiler options such as `target`,
|
|
290
|
+
`sourceMaps`, `sourceMapSourcePaths`, and `minify` live at the top level
|
|
291
|
+
- `remix/headers` — `SuperHeaders` plus typed header parsers and builders. Use the default export
|
|
292
|
+
when you want a `Headers` subclass with typed accessors like `headers.contentType`,
|
|
293
|
+
`headers.cacheControl`, and `headers.setCookie`; use named classes such as `CacheControl`,
|
|
294
|
+
`ContentDisposition`, and `Vary` when working with individual header values
|
|
239
295
|
- `remix/response/redirect` — `redirect(href, status?)`. Use for the canonical "POST then redirect"
|
|
240
296
|
pattern and other location changes
|
|
241
297
|
- `remix/response/html` — `createHtmlResponse`. Use when you need an HTML `Response` from a string
|
|
@@ -244,10 +300,14 @@ what it exports. Open the linked reference file when you need full examples.
|
|
|
244
300
|
the global `compression()` middleware
|
|
245
301
|
- `remix/response/file` — file-download responses. Use for `Content-Disposition: attachment`
|
|
246
302
|
responses
|
|
247
|
-
- `remix/route-pattern` — low-level URL matching and generation. Use
|
|
248
|
-
patterns outside the router (
|
|
303
|
+
- `remix/route-pattern` — low-level URL matching and generation. Use `RoutePattern` or
|
|
304
|
+
`createMatcher` when working with raw patterns outside the router. `href(...)` encodes pathname
|
|
305
|
+
and search params for you, and `match(...)` returns decoded params
|
|
306
|
+
- `remix/route-pattern/specificity` — pattern ranking helpers. Use only when building custom
|
|
307
|
+
matcher or reporting logic outside the normal router/matcher APIs
|
|
249
308
|
- `remix/fetch-proxy` — Fetch-based HTTP proxying. Use to forward a request to another origin; pass
|
|
250
|
-
`xForwardedHeaders` when the upstream needs forwarded proto, host, and port
|
|
309
|
+
`xForwardedHeaders` when the upstream needs forwarded proto, host, and port. It also rewrites
|
|
310
|
+
proxied `Set-Cookie` domain/path attributes by default
|
|
251
311
|
|
|
252
312
|
### Data, Validation, and Persistence
|
|
253
313
|
|
|
@@ -260,9 +320,11 @@ what it exports. Open the linked reference file when you need full examples.
|
|
|
260
320
|
Use when input arrives as a string but should be a typed value
|
|
261
321
|
- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use
|
|
262
322
|
in actions that read browser forms
|
|
323
|
+
- `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to
|
|
324
|
+
refer to itself or another schema that is declared later
|
|
263
325
|
- `remix/data-table` — typed tables and a `Database` interface. Use for `table`, `column`,
|
|
264
326
|
`createDatabase` when modeling persisted data
|
|
265
|
-
- `remix/data-table
|
|
327
|
+
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — adapters.
|
|
266
328
|
Use to back `createDatabase` with a real engine. SQLite accepts Node, Bun, and compatible
|
|
267
329
|
synchronous clients with the shared `prepare`/`exec` surface
|
|
268
330
|
- `remix/data-table/migrations` — migration authoring and runners. Use for `createMigration`,
|
|
@@ -271,27 +333,29 @@ what it exports. Open the linked reference file when you need full examples.
|
|
|
271
333
|
apply migrations
|
|
272
334
|
- `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses
|
|
273
335
|
need set or comparison logic
|
|
336
|
+
- `remix/data-table/sql-helpers` — SQL helper utilities for adapter or advanced query work. Avoid
|
|
337
|
+
this in normal app code unless you are intentionally working below the table/query API
|
|
274
338
|
|
|
275
339
|
### Auth, Sessions, and Cookies
|
|
276
340
|
|
|
277
341
|
- `remix/session` — the `Session` object: `get`, `set`, `flash`, `unset`, `regenerateId`. Use for
|
|
278
342
|
any per-browser state where tampering would be a bug (login, "I submitted this form already",
|
|
279
343
|
cart, flash messages)
|
|
280
|
-
- `remix/session
|
|
344
|
+
- `remix/middleware/session` — `session(cookie, storage)`. Use to wire a session cookie and
|
|
281
345
|
storage backend into the root middleware stack
|
|
282
|
-
- `remix/session/fs
|
|
346
|
+
- `remix/session-storage/fs`, `remix/session-storage/memory`, `remix/session-storage/cookie` —
|
|
283
347
|
storage backends. Use `fs-storage` for single-process apps, `memory-storage` for tests,
|
|
284
348
|
`cookie-storage` for stateless deployments where data fits in a cookie
|
|
285
|
-
- `remix/session-storage
|
|
349
|
+
- `remix/session-storage/redis` — Redis-backed storage. Use for multi-process or multi-host
|
|
286
350
|
deployments
|
|
287
|
-
- `remix/session-storage
|
|
351
|
+
- `remix/session-storage/memcache` — Memcache-backed storage. Same multi-host use case as Redis
|
|
288
352
|
- `remix/cookie` — `createCookie` for plain signed/unsigned cookies. Use for non-sensitive
|
|
289
353
|
preferences where the client is allowed to control the value (theme, locale, dismissed banner).
|
|
290
354
|
For state where tampering matters, prefer `remix/session`
|
|
291
355
|
- `remix/auth` — credentials, OAuth, OIDC, and Atmosphere providers. Use to define how identity is
|
|
292
356
|
verified, start/finish external login, and refresh stored OAuth/OIDC token bundles with
|
|
293
357
|
`refreshExternalAuth(...)`
|
|
294
|
-
- `remix/auth
|
|
358
|
+
- `remix/middleware/auth` — `auth({ schemes })`, `requireAuth`, the `Auth` context key. Use to
|
|
295
359
|
resolve identity into the request context and to gate routes
|
|
296
360
|
|
|
297
361
|
### UI, Hydration, and Browser Behavior
|
|
@@ -299,62 +363,75 @@ what it exports. Open the linked reference file when you need full examples.
|
|
|
299
363
|
- `remix/ui` — the component runtime: components, core mixins, `clientEntry`, `run`, `<Frame>`,
|
|
300
364
|
navigation helpers, and `createRoot`. Use for app UI behavior
|
|
301
365
|
- `remix/ui/server` — server rendering: `renderToStream`, `renderToString`. Use in the
|
|
302
|
-
`render
|
|
366
|
+
`app/actions/render.tsx` helper that returns HTML responses
|
|
303
367
|
- `remix/ui/animation` — animation APIs: `animateEntrance`, `animateExit`, `animateLayout`,
|
|
304
368
|
`spring`, `tween`, and `easings`
|
|
305
|
-
- `remix/ui/<primitive>` — UI primitives, mixins, glyphs, and theme helpers.
|
|
306
|
-
`remix/ui/accordion`, `remix/ui/
|
|
369
|
+
- `remix/ui/<primitive>` — UI primitives, mixins, glyphs, and theme helpers. Current subpaths
|
|
370
|
+
include `remix/ui/accordion`, `remix/ui/anchor`, `remix/ui/breadcrumbs`, `remix/ui/button`,
|
|
371
|
+
`remix/ui/combobox`, `remix/ui/glyph`, `remix/ui/listbox`, `remix/ui/menu`,
|
|
372
|
+
`remix/ui/popover`, `remix/ui/scroll-lock`, `remix/ui/select`, `remix/ui/separator`, and
|
|
373
|
+
`remix/ui/theme`
|
|
307
374
|
- `remix/ui/test` — component test rendering helpers such as `render`
|
|
308
|
-
- `remix/ui/jsx-runtime` — JSX transform
|
|
309
|
-
imported directly
|
|
375
|
+
- `remix/ui/jsx-runtime` and `remix/ui/jsx-dev-runtime` — JSX transform targets. Configured in
|
|
376
|
+
`tsconfig.json`, rarely imported directly
|
|
310
377
|
- `remix/html-template` — escaped HTML template literals. Use when generating HTML outside the
|
|
311
378
|
component system (RSS feeds, email bodies, error pages)
|
|
312
379
|
- `remix/file-storage` — backend-agnostic `File` storage interface. Use as the type bound for
|
|
313
380
|
upload destinations
|
|
314
|
-
- `remix/file-storage/fs`, `remix/file-storage/memory`, `remix/file-storage
|
|
381
|
+
- `remix/file-storage/fs`, `remix/file-storage/memory`, `remix/file-storage/s3` — storage
|
|
315
382
|
backends. Use to implement an upload destination
|
|
316
383
|
|
|
317
384
|
### Middleware
|
|
318
385
|
|
|
319
|
-
- `remix/static
|
|
386
|
+
- `remix/middleware/static` — `staticFiles(dir)`. Use to serve files from `public/` exactly as
|
|
320
387
|
they exist on disk
|
|
321
|
-
- `remix/form-data
|
|
388
|
+
- `remix/middleware/form-data` — `formData()`. Use to parse `FormData` once and expose it via
|
|
322
389
|
`get(FormData)` instead of calling `await request.formData()` in each action
|
|
323
390
|
- `remix/form-data-parser` — lower-level `parseFormData`, `FileUpload`. Use when implementing
|
|
324
391
|
custom upload handlers. Upload handler errors propagate directly
|
|
325
392
|
- `remix/multipart-parser` and `remix/multipart-parser/node` — low-level multipart stream parsing.
|
|
326
393
|
`MultipartPart.headers` is a plain object keyed by lower-case header name; read values with
|
|
327
394
|
bracket notation such as `part.headers['content-type']`
|
|
328
|
-
- `remix/compression
|
|
329
|
-
- `remix/logger
|
|
395
|
+
- `remix/middleware/compression` — `compression()`. Use globally for text-like responses
|
|
396
|
+
- `remix/middleware/logger` — `logger()`. Use in development for request logs; pass `colors` to
|
|
330
397
|
force terminal color output on or off
|
|
331
|
-
- `remix/method-override
|
|
398
|
+
- `remix/middleware/method-override` — `methodOverride()`. Use when HTML forms need `PUT`,
|
|
332
399
|
`PATCH`, or `DELETE`
|
|
333
|
-
- `remix/async-context
|
|
400
|
+
- `remix/middleware/async-context` — `asyncContext()`, `getContext()`. Use when helpers outside
|
|
334
401
|
actions need request context without threading it through every call
|
|
335
|
-
- `remix/cors
|
|
336
|
-
- `remix/csrf
|
|
402
|
+
- `remix/middleware/cors` — `cors(opts?)`. Use for endpoints called cross-origin
|
|
403
|
+
- `remix/middleware/csrf` — `csrf(opts?)`. Use when session-backed forms mutate state and need
|
|
337
404
|
synchronizer-token CSRF protection
|
|
338
|
-
- `remix/cop
|
|
405
|
+
- `remix/middleware/cop` — cross-origin protection. Use to reject unsafe cross-origin browser
|
|
339
406
|
requests
|
|
340
407
|
|
|
341
408
|
### Test
|
|
342
409
|
|
|
343
410
|
- `remix/test` — `describe`, `it`, and lifecycle hooks. Use as the test framework
|
|
344
411
|
- `remix/test/cli` — programmatic test runner APIs such as `runRemixTest`
|
|
412
|
+
- `remix/node-fetch-server/test` — `createTestServer` for end-to-end tests that need a real local
|
|
413
|
+
HTTP server around a Fetch handler
|
|
345
414
|
- `remix/cli` — programmatic Remix CLI API. Use the `remix` executable for project commands such
|
|
346
|
-
as `remix test`, `remix routes`, and `remix
|
|
415
|
+
as `remix test`, `remix routes`, `remix doctor`, and `remix version`
|
|
347
416
|
- `remix/assert` — assertion helpers. Use in place of `node:assert` so messages render cleanly
|
|
348
417
|
in the runner
|
|
349
418
|
- `remix/terminal` — ANSI styles, color detection, style factories, and testable terminal streams.
|
|
350
419
|
Use for CLIs and terminal output instead of hand-rolled escape sequences
|
|
420
|
+
- `remix/fs` — small filesystem helpers such as `openLazyFile` and `writeFile`. Use in Node-only
|
|
421
|
+
app or tooling code when you need lazy file responses or safe file writes
|
|
422
|
+
- `remix/lazy-file` — `LazyFile` primitives and byte-range helpers. Use when implementing file or
|
|
423
|
+
range responses below the higher-level response/file helpers
|
|
424
|
+
- `remix/mime` — content-type and MIME detection helpers. Use instead of maintaining app-local
|
|
425
|
+
extension maps
|
|
426
|
+
- `remix/tar-parser` — streaming tar parsing. Use for import/export tooling that consumes tar
|
|
427
|
+
archives
|
|
351
428
|
|
|
352
429
|
## Canonical Patterns
|
|
353
430
|
|
|
354
431
|
### Define routes first
|
|
355
432
|
|
|
356
433
|
```typescript
|
|
357
|
-
import { form, get, post, resources, route } from 'remix/
|
|
434
|
+
import { form, get, post, resources, route } from 'remix/routes'
|
|
358
435
|
|
|
359
436
|
export const routes = route({
|
|
360
437
|
home: '/',
|
|
@@ -377,12 +454,11 @@ export const routes = route({
|
|
|
377
454
|
### Type controllers against the route contract
|
|
378
455
|
|
|
379
456
|
```typescript
|
|
380
|
-
import
|
|
457
|
+
import { createController } from 'remix/router'
|
|
381
458
|
|
|
382
|
-
import type { AppContext } from '../router.ts'
|
|
383
459
|
import { routes } from '../routes.ts'
|
|
384
460
|
|
|
385
|
-
export default {
|
|
461
|
+
export default createController(routes.books, {
|
|
386
462
|
actions: {
|
|
387
463
|
async index({ get }) {
|
|
388
464
|
let db = get(Database)
|
|
@@ -396,30 +472,38 @@ export default {
|
|
|
396
472
|
return render(<BookShowPage book={book} />)
|
|
397
473
|
},
|
|
398
474
|
},
|
|
399
|
-
}
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Register Controllers Explicitly
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import { createRouter } from 'remix/router'
|
|
482
|
+
|
|
483
|
+
import rootController from './actions/controller.tsx'
|
|
484
|
+
import adminController from './actions/admin/controller.tsx'
|
|
485
|
+
import adminBooksController from './actions/admin/books/controller.tsx'
|
|
486
|
+
import authController from './actions/auth/controller.tsx'
|
|
487
|
+
import authLoginController from './actions/auth/login/controller.tsx'
|
|
488
|
+
import booksController from './actions/books/controller.tsx'
|
|
489
|
+
import contactController from './actions/contact/controller.tsx'
|
|
490
|
+
import { routes } from './routes.ts'
|
|
491
|
+
|
|
492
|
+
export const router = createRouter({ middleware })
|
|
493
|
+
|
|
494
|
+
router.map(routes, rootController)
|
|
495
|
+
router.map(routes.contact, contactController)
|
|
496
|
+
router.map(routes.books, booksController)
|
|
497
|
+
router.map(routes.auth, authController)
|
|
498
|
+
router.map(routes.auth.login, authLoginController)
|
|
499
|
+
router.map(routes.admin, adminController)
|
|
500
|
+
router.map(routes.admin.books, adminBooksController)
|
|
400
501
|
```
|
|
401
502
|
|
|
402
503
|
### Compose middleware deliberately
|
|
403
504
|
|
|
404
505
|
```typescript
|
|
405
|
-
import {
|
|
406
|
-
createRouter,
|
|
407
|
-
type AnyParams,
|
|
408
|
-
type MiddlewareContext,
|
|
409
|
-
type WithParams,
|
|
410
|
-
} from 'remix/fetch-router'
|
|
411
|
-
|
|
412
|
-
export type RootMiddleware = [
|
|
413
|
-
ReturnType<typeof formData>,
|
|
414
|
-
ReturnType<typeof session>,
|
|
415
|
-
ReturnType<typeof loadDatabase>,
|
|
416
|
-
ReturnType<typeof loadAuth>,
|
|
417
|
-
]
|
|
418
|
-
|
|
419
|
-
export type AppContext<params extends AnyParams = AnyParams> = WithParams<
|
|
420
|
-
MiddlewareContext<RootMiddleware>,
|
|
421
|
-
params
|
|
422
|
-
>
|
|
506
|
+
import { createRouter } from 'remix/router'
|
|
423
507
|
|
|
424
508
|
let middleware = []
|
|
425
509
|
|
|
@@ -439,21 +523,24 @@ middleware.push(loadAuth())
|
|
|
439
523
|
let router = createRouter({ middleware })
|
|
440
524
|
```
|
|
441
525
|
|
|
442
|
-
###
|
|
526
|
+
### Validate, mutate, and respond
|
|
443
527
|
|
|
444
528
|
```typescript
|
|
529
|
+
import { createController } from 'remix/router'
|
|
445
530
|
import { redirect } from 'remix/response/redirect'
|
|
446
531
|
import * as s from 'remix/data-schema'
|
|
447
532
|
import * as f from 'remix/data-schema/form-data'
|
|
448
533
|
import { Session } from 'remix/session'
|
|
449
534
|
import { Database } from 'remix/data-table'
|
|
450
535
|
|
|
536
|
+
import { routes } from '../routes.ts'
|
|
537
|
+
|
|
451
538
|
let bookSchema = f.object({
|
|
452
539
|
slug: f.field(s.string()),
|
|
453
540
|
title: f.field(s.string()),
|
|
454
541
|
})
|
|
455
542
|
|
|
456
|
-
export default {
|
|
543
|
+
export default createController(routes.books, {
|
|
457
544
|
actions: {
|
|
458
545
|
async create({ get }) {
|
|
459
546
|
let parsed = s.parseSafe(bookSchema, get(FormData))
|
|
@@ -470,7 +557,7 @@ export default {
|
|
|
470
557
|
return redirect(routes.books.show.href({ slug: book.slug }))
|
|
471
558
|
},
|
|
472
559
|
},
|
|
473
|
-
}
|
|
560
|
+
})
|
|
474
561
|
```
|
|
475
562
|
|
|
476
563
|
This shape works without JavaScript, returns a `Response` for every outcome, and is ready for
|
package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md
RENAMED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
How to serve browser scripts and styles from source. Read this when the task involves:
|
|
6
6
|
|
|
7
|
-
- Configuring `createAssetServer` (`fileMap`, `allow`, `deny`, fingerprinting,
|
|
7
|
+
- Configuring `createAssetServer` (`basePath`, `fileMap`, `allow`, `deny`, fingerprinting,
|
|
8
|
+
compiler options)
|
|
8
9
|
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source
|
|
9
10
|
assets that need import rewriting, preloads, or fingerprinted URLs
|
|
10
11
|
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
|
|
@@ -26,18 +27,22 @@ preloads, sourcemaps, or fingerprinted URLs.
|
|
|
26
27
|
## Default Pattern
|
|
27
28
|
|
|
28
29
|
```typescript
|
|
29
|
-
import * as path from 'node:path'
|
|
30
|
-
|
|
31
30
|
import { createAssetServer } from 'remix/assets'
|
|
32
|
-
import {
|
|
31
|
+
import { createController } from 'remix/router'
|
|
32
|
+
import { get, route } from 'remix/routes'
|
|
33
|
+
|
|
34
|
+
export const routes = route({
|
|
35
|
+
assets: get('/assets/*path'),
|
|
36
|
+
})
|
|
33
37
|
|
|
34
38
|
let assetServer = createAssetServer({
|
|
35
|
-
|
|
39
|
+
basePath: '/assets',
|
|
40
|
+
rootDir: process.cwd(),
|
|
36
41
|
fileMap: {
|
|
37
|
-
'
|
|
38
|
-
'
|
|
42
|
+
'app/*path': 'app/*path',
|
|
43
|
+
'node_modules/*path': 'node_modules/*path',
|
|
39
44
|
},
|
|
40
|
-
allow: ['app/assets/**', '
|
|
45
|
+
allow: ['app/assets/**', 'node_modules/**'],
|
|
41
46
|
deny: ['app/**/*.server.*'],
|
|
42
47
|
target: { es: '2020', chrome: '109', safari: '16.4' },
|
|
43
48
|
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
|
|
@@ -49,10 +54,12 @@ let assetServer = createAssetServer({
|
|
|
49
54
|
},
|
|
50
55
|
})
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
export default createController(routes, {
|
|
58
|
+
actions: {
|
|
59
|
+
async assets({ request }) {
|
|
60
|
+
return (await assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
|
|
61
|
+
},
|
|
62
|
+
},
|
|
56
63
|
})
|
|
57
64
|
```
|
|
58
65
|
|
|
@@ -62,8 +69,9 @@ router.get('/assets/*path', ({ request }) => {
|
|
|
62
69
|
- Add a `deny` list for server-only modules such as `*.server.*`, private config, or other files
|
|
63
70
|
that should never be exposed.
|
|
64
71
|
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
|
|
65
|
-
- `
|
|
66
|
-
|
|
72
|
+
- `basePath` is the public URL namespace handled by the asset server.
|
|
73
|
+
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path
|
|
74
|
+
patterns. They use `route-pattern` syntax on both sides.
|
|
67
75
|
- Keep the same wildcard params on both sides of a `fileMap` entry so import rewriting can map
|
|
68
76
|
source files back to public URLs.
|
|
69
77
|
- CSS files are compiled and served alongside scripts. Local CSS `@import` rules are rewritten and
|
|
@@ -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() {
|