@remix-run/cli 0.1.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.
Files changed (110) hide show
  1. package/README.md +0 -3
  2. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  3. package/dist/lib/bootstrap-project.js +32 -9
  4. package/dist/lib/cli.d.ts +29 -0
  5. package/dist/lib/cli.d.ts.map +1 -1
  6. package/dist/lib/cli.js +33 -14
  7. package/dist/lib/commands/completion.d.ts.map +1 -1
  8. package/dist/lib/commands/completion.js +5 -1
  9. package/dist/lib/commands/doctor.js +18 -18
  10. package/dist/lib/commands/help.d.ts.map +1 -1
  11. package/dist/lib/commands/help.js +9 -33
  12. package/dist/lib/commands/routes.js +3 -3
  13. package/dist/lib/commands/test.d.ts +1 -1
  14. package/dist/lib/commands/test.d.ts.map +1 -1
  15. package/dist/lib/commands/test.js +8 -4
  16. package/dist/lib/completion.d.ts.map +1 -1
  17. package/dist/lib/completion.js +4 -106
  18. package/dist/lib/controller-files.d.ts +0 -1
  19. package/dist/lib/controller-files.d.ts.map +1 -1
  20. package/dist/lib/controller-files.js +3 -5
  21. package/dist/lib/controller-ownership.d.ts +9 -9
  22. package/dist/lib/controller-ownership.d.ts.map +1 -1
  23. package/dist/lib/controller-ownership.js +56 -91
  24. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  25. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  26. package/dist/lib/doctor/controller-findings.js +15 -87
  27. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  28. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  29. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  30. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  31. package/dist/lib/doctor/controller-placeholders.js +18 -149
  32. package/dist/lib/doctor/controllers.js +1 -1
  33. package/dist/lib/doctor/project.js +60 -52
  34. package/dist/lib/doctor/types.d.ts +2 -2
  35. package/dist/lib/doctor/types.d.ts.map +1 -1
  36. package/dist/lib/errors.d.ts +0 -6
  37. package/dist/lib/errors.d.ts.map +1 -1
  38. package/dist/lib/errors.js +0 -11
  39. package/dist/lib/load-route-map-worker.js +17 -9
  40. package/dist/lib/route-map.d.ts +1 -1
  41. package/dist/lib/route-map.d.ts.map +1 -1
  42. package/dist/lib/route-map.js +29 -17
  43. package/package.json +4 -5
  44. package/src/lib/bootstrap-project.ts +39 -13
  45. package/src/lib/cli.ts +46 -15
  46. package/src/lib/commands/completion.ts +6 -1
  47. package/src/lib/commands/doctor.ts +18 -21
  48. package/src/lib/commands/help.ts +9 -43
  49. package/src/lib/commands/routes.ts +3 -3
  50. package/src/lib/commands/test.ts +10 -4
  51. package/src/lib/completion.ts +4 -151
  52. package/src/lib/controller-files.ts +4 -8
  53. package/src/lib/controller-ownership.ts +78 -141
  54. package/src/lib/doctor/controller-findings.ts +20 -97
  55. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  56. package/src/lib/doctor/controller-placeholders.ts +17 -189
  57. package/src/lib/doctor/controllers.ts +1 -1
  58. package/src/lib/doctor/project.ts +60 -52
  59. package/src/lib/doctor/types.ts +1 -5
  60. package/src/lib/errors.ts +0 -12
  61. package/src/lib/load-route-map-worker.ts +19 -10
  62. package/src/lib/route-map.ts +61 -16
  63. package/template/.agents/skills/remix/SKILL.md +588 -0
  64. package/template/.agents/skills/remix/references/animate-elements.md +195 -0
  65. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
  66. package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
  67. package/template/.agents/skills/remix/references/component-model.md +282 -0
  68. package/template/.agents/skills/remix/references/create-mixins.md +158 -0
  69. package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
  70. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  71. package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
  72. package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  73. package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
  74. package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
  75. package/{bootstrap → template}/AGENTS.md +13 -8
  76. package/template/README.md +29 -0
  77. package/template/app/actions/controller.tsx +18 -0
  78. package/template/app/assets/entry.ts +8 -0
  79. package/template/app/assets/prompt-button.tsx +163 -0
  80. package/template/app/assets.ts +20 -0
  81. package/template/app/middleware/render.tsx +43 -0
  82. package/template/app/router.ts +20 -0
  83. package/template/app/routes.ts +6 -0
  84. package/template/app/ui/document.tsx +33 -0
  85. package/template/app/ui/scaffold-home-page.tsx +527 -0
  86. package/template/gitignore +4 -0
  87. package/{bootstrap → template}/package.json +5 -6
  88. package/template/public/favicon.svg +11 -0
  89. package/{bootstrap → template}/server.ts +4 -5
  90. package/{bootstrap → template}/tsconfig.json +3 -3
  91. package/bootstrap/README.md +0 -27
  92. package/bootstrap/app/controllers/auth.tsx +0 -21
  93. package/bootstrap/app/controllers/home.tsx +0 -26
  94. package/bootstrap/app/router.ts +0 -10
  95. package/bootstrap/app/routes.ts +0 -6
  96. package/bootstrap/app/ui/document.tsx +0 -21
  97. package/bootstrap/app/ui/layout.tsx +0 -22
  98. package/bootstrap/app/utils/render.tsx +0 -7
  99. package/dist/lib/commands/skills.d.ts +0 -6
  100. package/dist/lib/commands/skills.d.ts.map +0 -1
  101. package/dist/lib/commands/skills.js +0 -222
  102. package/dist/lib/skills-cache.d.ts +0 -19
  103. package/dist/lib/skills-cache.d.ts.map +0 -1
  104. package/dist/lib/skills-cache.js +0 -89
  105. package/dist/lib/skills.d.ts +0 -30
  106. package/dist/lib/skills.d.ts.map +0 -1
  107. package/dist/lib/skills.js +0 -441
  108. package/src/lib/commands/skills.ts +0 -306
  109. package/src/lib/skills-cache.ts +0 -140
  110. package/src/lib/skills.ts +0 -706
@@ -0,0 +1,391 @@
1
+ # Routing and Controllers
2
+
3
+ ## What This Covers
4
+
5
+ Patterns for declaring URLs, handling requests, and wiring routes to controllers. Read this when
6
+ the task involves:
7
+
8
+ - Defining or changing the URL surface of the app
9
+ - Writing or reorganizing controllers and actions
10
+ - Reading request data (`params`, `url`, `request`, context values)
11
+ - Returning a `Response` for HTML, redirects, JSON, or errors
12
+ - Generating internal URLs with `.href()`
13
+
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`.
17
+
18
+ ## Route Builders
19
+
20
+ Import all route builders from `remix/routes`.
21
+
22
+ ### `route(prefix, map)` — nested route group
23
+
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).
28
+
29
+ ```typescript
30
+ import { route, get, post } from 'remix/routes'
31
+
32
+ export const routes = route({
33
+ home: '/',
34
+
35
+ // Plain object — no shared prefix, each leaf has an absolute path.
36
+ books: {
37
+ index: '/books',
38
+ show: '/books/:slug',
39
+ },
40
+
41
+ // route('auth', ...) — every leaf is prefixed with /auth.
42
+ auth: route('auth', {
43
+ login: get('login'),
44
+ logout: post('logout'),
45
+ }),
46
+ })
47
+ ```
48
+
49
+ ### Leaf route builders
50
+
51
+ | Builder | HTTP method | Example |
52
+ | -------------- | ----------- | -------------------- |
53
+ | `get(path)` | GET | `get('/search')` |
54
+ | `post(path)` | POST | `post('/logout')` |
55
+ | `put(path)` | PUT | `put('/api/update')` |
56
+ | `del(path)` | DELETE | `del('/api/remove')` |
57
+ | String literal | ANY | `'/about'` |
58
+
59
+ ### `form(path, options?)` — form route
60
+
61
+ Creates a GET + POST pair for HTML form workflows. Expands to an `index` (GET) and an `action`
62
+ (POST) by default.
63
+
64
+ ```typescript
65
+ contact: form('contact')
66
+ // Produces routes.contact.index (GET /contact) and routes.contact.action (POST /contact)
67
+
68
+ settings: form('settings', { formMethod: 'PUT', names: { action: 'update' } })
69
+ // Produces routes.settings.index (GET) and routes.settings.update (PUT)
70
+ ```
71
+
72
+ ### `resources(name, options?)` — REST resources
73
+
74
+ Expands to conventional CRUD routes: `index`, `new`, `create`, `show`, `edit`, `update`, `destroy`.
75
+
76
+ ```typescript
77
+ books: resources('books', { param: 'bookId' })
78
+ // GET /books, GET /books/new, POST /books, GET /books/:bookId, ...
79
+
80
+ orders: resources('orders', { only: ['index', 'show'], param: 'orderId' })
81
+ // GET /orders, GET /orders/:orderId
82
+ ```
83
+
84
+ ### URL generation with `.href()`
85
+
86
+ Route objects expose `.href()` for type-safe URL generation:
87
+
88
+ ```typescript
89
+ redirect(routes.home.href())
90
+ redirect(routes.account.orders.show.href({ orderId: '42' }))
91
+ ```
92
+
93
+ ## Actions
94
+
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:
98
+
99
+ ```typescript
100
+ import { createAction } from 'remix/router'
101
+
102
+ import { routes } from '../routes.ts'
103
+
104
+ export const search = createAction(routes.search, {
105
+ async handler({ url }) {
106
+ let query = url.searchParams.get('q') ?? ''
107
+ let results = await searchIndex(query)
108
+ return render(<SearchPage query={query} results={results} />)
109
+ },
110
+ })
111
+ ```
112
+
113
+ The handler receives a context object with:
114
+
115
+ - `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`)
116
+ - `params` — typed route params
117
+ - `url` — the request URL
118
+ - `request` — the raw `Request`
119
+
120
+ Actions with inline middleware:
121
+
122
+ ```typescript
123
+ import { requireAuth } from 'remix/middleware/auth'
124
+
125
+ router.get(routes.account.index, {
126
+ middleware: [requireAuth()],
127
+ handler: accountAction.handler,
128
+ })
129
+ ```
130
+
131
+ ## Returning Responses
132
+
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.
135
+
136
+ ### Render HTML
137
+
138
+ For pages, render a component tree and return the resulting `Response`:
139
+
140
+ ```typescript
141
+ async handler({ get }) {
142
+ let db = get(Database)
143
+ let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
144
+ return render(<IndexPage books={books} />)
145
+ }
146
+ ```
147
+
148
+ ### Redirect after a mutation
149
+
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:
152
+
153
+ ```typescript
154
+ import { redirect } from 'remix/response/redirect'
155
+
156
+ async create({ get }) {
157
+ let formData = get(FormData)
158
+ let parsed = s.parseSafe(bookSchema, formData)
159
+ if (!parsed.success) {
160
+ return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
161
+ }
162
+
163
+ let db = get(Database)
164
+ let book = await db.create(books, parsed.value)
165
+
166
+ return redirect(routes.books.show.href({ slug: book.slug }), 303)
167
+ }
168
+ ```
169
+
170
+ This pattern works without JavaScript and stays compatible with `clientEntry(...)` enhancements
171
+ on top.
172
+
173
+ ### Return an error response
174
+
175
+ For expected failures — validation, conflict, not found — return a `Response` directly. Reserve
176
+ thrown errors for genuinely unexpected failures.
177
+
178
+ ```typescript
179
+ async show({ get, params }) {
180
+ let db = get(Database)
181
+ let book = await db.find(books, params.bookId)
182
+ if (!book) return new Response('Not Found', { status: 404 })
183
+ return render(<ShowPage book={book} />)
184
+ }
185
+ ```
186
+
187
+ For form re-rendering with errors, return the page component with the parsed issues:
188
+
189
+ ```typescript
190
+ let formData = get(FormData)
191
+ let parsed = s.parseSafe(signupSchema, formData)
192
+ if (!parsed.success) {
193
+ return render(<SignupPage errors={parsed.issues} values={Object.fromEntries(formData)} />, {
194
+ status: 400,
195
+ })
196
+ }
197
+ ```
198
+
199
+ ### Return JSON
200
+
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:
204
+
205
+ ```typescript
206
+ import Headers from 'remix/headers'
207
+
208
+ let headers = new Headers()
209
+ headers.contentType = { mediaType: 'application/json', charset: 'utf-8' }
210
+ headers.cacheControl = { noStore: true }
211
+
212
+ return new Response(JSON.stringify({ results }), {
213
+ headers,
214
+ })
215
+ ```
216
+
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.
221
+
222
+ ## Controllers
223
+
224
+ A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct
225
+ leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not
226
+ belong inside a controller's `actions`; map those route maps with their own controllers.
227
+
228
+ Configure `RouterTypes.context` with your app context in the router module, then use
229
+ `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your
230
+ middleware stack without repeating a type clause on every controller.
231
+
232
+ ```typescript
233
+ import { createController } from 'remix/router'
234
+
235
+ import { routes } from '../routes.ts'
236
+
237
+ export default createController(routes.books, {
238
+ actions: {
239
+ async index({ get }) {
240
+ let db = get(Database)
241
+ let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
242
+ return render(<IndexPage items={items} />)
243
+ },
244
+
245
+ async show({ get, params }) {
246
+ let db = get(Database)
247
+ let book = await db.find(books, params.bookId)
248
+ if (!book) return new Response('Not Found', { status: 404 })
249
+ return render(<ShowPage book={book} />)
250
+ },
251
+ },
252
+ })
253
+ ```
254
+
255
+ ### Root controller
256
+
257
+ The root route map uses `app/actions/controller.tsx` and owns only top-level leaf routes:
258
+
259
+ ```typescript
260
+ // routes.ts
261
+ export const routes = route({
262
+ assets: get('/assets/*path'),
263
+ home: '/',
264
+ account: route('account', {
265
+ index: '/',
266
+ settings: form('settings', { formMethod: 'PUT', names: { action: 'update' } }),
267
+ }),
268
+ })
269
+
270
+ // app/actions/controller.tsx
271
+ export default createController(routes, {
272
+ actions: {
273
+ async assets({ request }) {
274
+ return (await assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
275
+ },
276
+ home() {
277
+ return render(<HomePage />)
278
+ },
279
+ },
280
+ })
281
+ ```
282
+
283
+ Because `account` is a nested route map, it is not an action key in the root controller.
284
+
285
+ ### Nested route maps
286
+
287
+ Nested route maps use their own controllers under `app/actions/<route-key>/controller.tsx`.
288
+ Directory names under `app/actions/` are route-map keys, not URL path segments.
289
+
290
+ ```typescript
291
+ // app/actions/account/controller.tsx
292
+ export default createController(routes.account, {
293
+ middleware: [requireAuth()],
294
+ actions: {
295
+ index() {
296
+ return render(<AccountPage />)
297
+ },
298
+ },
299
+ })
300
+
301
+ // app/actions/account/settings/controller.tsx
302
+ export default createController(routes.account.settings, {
303
+ middleware: [requireAuth()],
304
+ actions: {
305
+ index() {
306
+ return render(<SettingsPage />)
307
+ },
308
+ update() {
309
+ return redirect(routes.account.index.href(), 303)
310
+ },
311
+ },
312
+ })
313
+ ```
314
+
315
+ Then map each route map explicitly:
316
+
317
+ ```typescript
318
+ import rootController from './actions/controller.tsx'
319
+ import accountController from './actions/account/controller.tsx'
320
+ import accountSettingsController from './actions/account/settings/controller.tsx'
321
+
322
+ let router = createRouter({ middleware })
323
+
324
+ router.map(routes, rootController)
325
+ router.map(routes.account, accountController)
326
+ router.map(routes.account.settings, accountSettingsController)
327
+ ```
328
+
329
+ ### Controller middleware
330
+
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.
333
+
334
+ ```typescript
335
+ export default createController(routes.admin, {
336
+ middleware: [requireAuth(), requireAdmin()],
337
+ actions: {
338
+ /* all actions require auth + admin */
339
+ },
340
+ })
341
+ ```
342
+
343
+ ## Registering Routes
344
+
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.
347
+
348
+ ```typescript
349
+ let router = createRouter({ middleware })
350
+
351
+ // Route maps → controllers
352
+ router.map(routes, rootController)
353
+ router.map(routes.contact, contactController)
354
+ router.map(routes.auth, authController)
355
+ router.map(routes.auth.login, authLoginController)
356
+ router.map(routes.admin, adminController)
357
+ router.map(routes.admin.books, adminBooksController)
358
+
359
+ // Leaf route → one-off action
360
+ router.get(routes.search, searchAction)
361
+ router.post(routes.logout, logoutAction)
362
+ ```
363
+
364
+ ## Typed Context
365
+
366
+ Define an `AppContext` type from your middleware stack, then make it the default context used by
367
+ `createAction()` and `createController()`:
368
+
369
+ ```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
+ >
383
+
384
+ declare module 'remix/router' {
385
+ interface RouterTypes {
386
+ context: AppContext
387
+ }
388
+ }
389
+ ```
390
+
391
+ This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.
@@ -0,0 +1,172 @@
1
+ # Testing
2
+
3
+ ## What This Covers
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:
7
+
8
+ - Driving the router with `router.fetch(new Request(...))` and asserting on the returned `Response`
9
+ - Building a fresh router per test for session, storage, or database isolation
10
+ - Rendering components into a real DOM with `render(...)` or `createRoot(...)`
11
+ - Configuring `remix test` discovery, excludes, and coverage
12
+ - Using adjacent CLI checks such as `remix routes`, `remix doctor`, and `remix version`
13
+ - Choosing which layer to test for a given behavior
14
+
15
+ For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see
16
+ `component-model.md`.
17
+
18
+ ## Two Shapes
19
+
20
+ Remix tests run with `remix test`, use `remix/test` for the test framework, and use
21
+ `remix/assert` for assertions. Two main shapes:
22
+
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.
27
+
28
+ ## Server / Router Tests
29
+
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.
33
+
34
+ ```ts
35
+ import * as assert from 'remix/assert'
36
+ import { describe, it } from 'remix/test'
37
+
38
+ import { createBookstoreRouter } from '../app/router.ts'
39
+ import { routes } from '../app/routes.ts'
40
+
41
+ describe('home', () => {
42
+ it('responds 200 with the home page', async () => {
43
+ let router = createBookstoreRouter()
44
+ let response = await router.fetch(new Request('http://localhost' + routes.home.href()))
45
+
46
+ assert.equal(response.status, 200)
47
+ assert.match(await response.text(), /Welcome to the Bookstore/)
48
+ })
49
+ })
50
+ ```
51
+
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.
56
+
57
+ ```ts
58
+ import { createMemorySessionStorage } from 'remix/session-storage/memory'
59
+ import { createCookie } from 'remix/cookie'
60
+
61
+ let router = createBookstoreRouter({
62
+ sessionCookie: createCookie('session', { secrets: ['test'] }),
63
+ sessionStorage: createMemorySessionStorage(),
64
+ })
65
+ ```
66
+
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`:
69
+
70
+ ```ts
71
+ import { createTestServer } from 'remix/node-fetch-server/test'
72
+
73
+ let server = await createTestServer((request) => router.fetch(request))
74
+ try {
75
+ let response = await fetch(new URL(routes.home.href(), server.baseUrl))
76
+ assert.equal(response.status, 200)
77
+ } finally {
78
+ await server.close()
79
+ }
80
+ ```
81
+
82
+ ## Test Runner Config
83
+
84
+ Configure discovery and coverage in `remix-test.config.ts` or with CLI flags:
85
+
86
+ ```ts
87
+ export default {
88
+ glob: {
89
+ test: '**/*.test{,.e2e}.{ts,tsx}',
90
+ e2e: '**/*.test.e2e.{ts,tsx}',
91
+ exclude: 'node_modules/**',
92
+ },
93
+ coverage: {
94
+ dir: '.coverage',
95
+ include: ['app/**/*.{ts,tsx}'],
96
+ exclude: ['app/**/*.test.{ts,tsx}'],
97
+ statements: 80,
98
+ lines: 80,
99
+ branches: 70,
100
+ functions: 80,
101
+ },
102
+ }
103
+ ```
104
+
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.
108
+
109
+ ## Component Tests
110
+
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.
115
+
116
+ ### Basic pattern
117
+
118
+ ```tsx
119
+ import * as assert from 'remix/assert'
120
+ import { render } from 'remix/ui/test'
121
+
122
+ let result = render(<Counter />)
123
+
124
+ let button = result.$('button')!
125
+ await result.act(() => button.click())
126
+
127
+ assert.match(result.container.textContent ?? '', /1/)
128
+ result.cleanup()
129
+ ```
130
+
131
+ ### Why act / flush
132
+
133
+ - **After initial render** — ensures event listeners are attached and the DOM is ready for
134
+ interaction.
135
+ - **After interactions** — applies updates from `handle.update()` calls triggered by events.
136
+ - **After async work resolves** — applies updates from resolved `queueTask(...)` callbacks.
137
+
138
+ ### Async operations
139
+
140
+ For components with async operations in `queueTask`, use `act(...)` after each async step:
141
+
142
+ ```tsx
143
+ let result = render(<AsyncLoader />)
144
+
145
+ assert.equal(result.container.textContent, 'Loading...')
146
+
147
+ await waitForFetch()
148
+ await result.act(() => {})
149
+
150
+ assert.equal(result.container.textContent, 'Expected data')
151
+ ```
152
+
153
+ ### Component removal
154
+
155
+ Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup
156
+ behavior:
157
+
158
+ ```tsx
159
+ let result = render(<MyComponent />)
160
+
161
+ assert.ok(result.$('.content'))
162
+
163
+ result.cleanup()
164
+ assert.throws(() => result.$('.content'), /cleaned up/)
165
+ ```
166
+
167
+ ### Guidelines
168
+
169
+ - Prefer real DOM interactions over mocking framework behavior.
170
+ - 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.
@@ -11,25 +11,30 @@ npm test
11
11
  npm run typecheck
12
12
  ```
13
13
 
14
+ ## Building Features
15
+
16
+ Refer to ./.agents/skills/remix/SKILL.md
17
+
14
18
  ## Starter Layout
15
19
 
16
- - `app/controllers/home.tsx` owns the home page
17
- - `app/controllers/auth.tsx` owns the auth page
20
+ - `app/actions/controller.tsx` owns the top-level route actions
18
21
  - `app/routes.ts` defines the route contract
19
22
  - `app/router.ts` wires routes to route handlers
20
- - `app/ui/` holds the shared document and layout wrappers
21
- - `app/utils/render.tsx` centralizes HTML response rendering
23
+ - `app/middleware/render.tsx` installs the request-scoped renderer used by actions
24
+ - `app/ui/` holds the shared document shell and home page UI
25
+ - `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer
26
+ - `public/` contains static files served from the app root
22
27
 
23
28
  ## Route Ownership
24
29
 
25
30
  - Start from `app/routes.ts` and map each route to the narrowest owner on disk.
26
- - Keep simple pages in flat files like `app/controllers/home.tsx` and `app/controllers/auth.tsx`.
27
- - Promote a route into a controller folder with `controller.tsx` only when it gains nested routes, multiple actions, or route-owned modules.
31
+ - Put top-level route actions in `app/actions/controller.tsx`.
32
+ - Add `app/actions/<route-key>/controller.tsx` for nested route maps that need their own actions or middleware.
28
33
  - Keep route-owned page modules next to the route that owns them.
29
- - Move shared UI to `app/ui/`, not `app/controllers/`.
34
+ - Move shared UI to `app/ui/`, not `app/actions/`.
30
35
 
31
36
  ## Build-Out Notes
32
37
 
33
- - This starter intentionally begins small; add directories like `app/data/`, `app/middleware/`, `public/`, and `test/` only when you need them.
38
+ - This starter intentionally begins small; add directories like `app/data/` and `test/` only when you need them.
34
39
  - Prefer putting code in the narrowest owner before introducing shared modules.
35
40
  - Avoid generic dumping-ground directories like `app/lib/` or `app/components/`.
@@ -0,0 +1,29 @@
1
+ # %%RMX_APP_DISPLAY_NAME%%
2
+
3
+ A minimal Remix application starter with a home page.
4
+
5
+ ## Starter Shape
6
+
7
+ - `app/actions/controller.tsx` owns the top-level route actions.
8
+ - `app/routes.ts` defines the route contract.
9
+ - `app/router.ts` wires routes to handlers.
10
+ - `app/middleware/render.tsx` installs the request-scoped renderer used by actions.
11
+ - `app/ui/` holds the shared document shell and home page UI.
12
+ - `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer.
13
+ - `public/` contains static files served from the app root.
14
+
15
+ ## Growing The App
16
+
17
+ - Put top-level route actions in `app/actions/controller.tsx`.
18
+ - Add `app/actions/<route-key>/controller.tsx` when a nested route map needs its own actions or middleware.
19
+ - Add directories like `app/data/` or `test/` when the app actually needs them.
20
+ - Move shared UI into `app/ui/` once more than one route needs it.
21
+
22
+ ## Commands
23
+
24
+ ```sh
25
+ npm i
26
+ npm run start
27
+ npm test
28
+ npm run typecheck
29
+ ```
@@ -0,0 +1,18 @@
1
+ import { createController } from 'remix/router'
2
+
3
+ import { assetServer } from '../assets.ts'
4
+ import { routes } from '../routes.ts'
5
+ import { HomePage } from '../ui/scaffold-home-page.tsx'
6
+
7
+ export default createController(routes, {
8
+ actions: {
9
+ async assets(context) {
10
+ return (
11
+ (await assetServer.fetch(context.request)) ?? new Response('Not Found', { status: 404 })
12
+ )
13
+ },
14
+ home(context) {
15
+ return context.render(<HomePage />)
16
+ },
17
+ },
18
+ })
@@ -0,0 +1,8 @@
1
+ import { run } from 'remix/ui'
2
+
3
+ run({
4
+ async loadModule(moduleUrl, exportName) {
5
+ let mod = await import(moduleUrl)
6
+ return mod[exportName]
7
+ },
8
+ })