@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,379 @@
1
+ # Data Access and Validation
2
+
3
+ ## What This Covers
4
+
5
+ How input becomes a value the app trusts, and how that value reaches storage. Read this when the
6
+ task involves:
7
+
8
+ - Defining database tables, columns, relations, and migrations
9
+ - Querying or mutating persisted data with `Database`
10
+ - Parsing and validating user input from forms, query strings, or external payloads
11
+ - Choosing between schema-level checks, table validation hooks, and migration-level constraints
12
+
13
+ For where validation runs in the request lifecycle, see `routing-and-controllers.md`. For session
14
+ or identity-bound writes, see `auth-and-sessions.md`.
15
+
16
+ ## Table Definitions (`remix/data-table`)
17
+
18
+ Define tables with typed columns, relations, and optional validation hooks:
19
+
20
+ ```typescript
21
+ import { belongsTo, column as c, hasMany, table } from 'remix/data-table'
22
+ import type { TableRow, TableRowWith } from 'remix/data-table'
23
+
24
+ export const books = table({
25
+ name: 'books',
26
+ columns: {
27
+ id: c.integer().primaryKey().autoIncrement(),
28
+ slug: c.text().notNull().unique(),
29
+ title: c.text().notNull(),
30
+ author: c.text().notNull(),
31
+ price: c.decimal(10, 2).notNull(),
32
+ genre: c.text().notNull(),
33
+ in_stock: c.boolean(),
34
+ },
35
+ })
36
+
37
+ export const orders = table({
38
+ name: 'orders',
39
+ columns: {
40
+ id: c.integer().primaryKey().autoIncrement(),
41
+ user_id: c.integer().notNull().references('users', 'id'),
42
+ total: c.decimal(10, 2).notNull(),
43
+ created_at: c.integer().notNull(),
44
+ },
45
+ relations: {
46
+ user: belongsTo('users', 'user_id'),
47
+ items: hasMany('order_items', 'order_id'),
48
+ },
49
+ })
50
+
51
+ export type Book = TableRow<typeof books>
52
+ export type Order = TableRow<typeof orders>
53
+ export type OrderWithItems = TableRowWith<typeof orders, 'items'>
54
+ ```
55
+
56
+ ### Column types
57
+
58
+ | Method | SQL type |
59
+ | ----------------------------- | ------------------ |
60
+ | `c.integer()` | INTEGER |
61
+ | `c.text()` | TEXT |
62
+ | `c.boolean()` | BOOLEAN |
63
+ | `c.decimal(precision, scale)` | DECIMAL |
64
+ | `c.enum([...])` | TEXT (string enum) |
65
+ | `c.uuid()` | UUID / TEXT |
66
+ | `c.varchar(length)` | VARCHAR |
67
+
68
+ Column modifiers: `.primaryKey()`, `.autoIncrement()`, `.notNull()`, `.unique()`,
69
+ `.references(table, column, fkName?)`, `.onDelete(action)`, `.default(value)`.
70
+
71
+ Composite primary keys go on the table option, not the column: `primaryKey: ['order_id', 'book_id']`.
72
+
73
+ ### Schema vs migrations
74
+
75
+ Column modifiers describe SQL constraints — the source of truth for them is your **migration**
76
+ files, where they generate the actual DDL. Runtime `table(...)` definitions in `app/data/schema.ts`
77
+ can use the same modifiers, or they can stay minimal (`c.integer()`, `c.text()`, ...) since the
78
+ runtime only needs the column shape and validation hooks. Two valid patterns:
79
+
80
+ - **Modifiers in both** — schema and migrations stay in sync visually; useful when you want
81
+ schema-level docs.
82
+ - **Bare columns in schema, full modifiers in migrations** — schema describes what the app reads
83
+ and writes; migrations own the DDL and constraints.
84
+
85
+ Pick one and apply it consistently across the app.
86
+
87
+ ### Table lifecycle hooks
88
+
89
+ Tables can define validation and lifecycle hooks:
90
+
91
+ - `validate` runs before `create` and `update` writes and should return either `{ value }` or
92
+ `{ issues }`
93
+ - `beforeWrite` can normalize or veto `create`/`update` values
94
+ - `afterWrite` observes completed `create`/`update` operations
95
+ - `beforeDelete` and `afterDelete` observe or veto deletes
96
+ - `afterRead` can normalize or reject row values after reads
97
+
98
+ ```typescript
99
+ export const books = table({
100
+ name: 'books',
101
+ columns: {
102
+ /* ... */
103
+ },
104
+ beforeWrite({ value }) {
105
+ if (typeof value.slug === 'string') {
106
+ return { value: { ...value, slug: value.slug.trim().toLowerCase() } }
107
+ }
108
+ return { value }
109
+ },
110
+ validate({ operation, value }) {
111
+ let issues = []
112
+ if (operation === 'create' && !value.slug) {
113
+ issues.push({ message: 'Slug is required.', path: ['slug'] })
114
+ }
115
+ return issues.length > 0 ? { issues } : { value }
116
+ },
117
+ afterRead({ value }) {
118
+ return { value }
119
+ },
120
+ })
121
+ ```
122
+
123
+ ## Database Setup
124
+
125
+ Create a database with an adapter and expose it via middleware:
126
+
127
+ ```typescript
128
+ import BetterSqlite3 from 'better-sqlite3'
129
+ import { createDatabase, Database } from 'remix/data-table'
130
+ import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
131
+
132
+ let sqlite = new BetterSqlite3('./db/app.db')
133
+ sqlite.pragma('foreign_keys = ON')
134
+ let adapter = createSqliteDatabaseAdapter(sqlite)
135
+ export let db = createDatabase(adapter)
136
+ ```
137
+
138
+ `createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec`
139
+ surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever
140
+ client fits the runtime instead of assuming `better-sqlite3` is required.
141
+
142
+ ### Database middleware
143
+
144
+ ```typescript
145
+ import type { Middleware } from 'remix/router'
146
+ import { Database } from 'remix/data-table'
147
+
148
+ export function loadDatabase(): Middleware {
149
+ return async (context, next) => {
150
+ context.set(Database, db)
151
+ return next()
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Querying
157
+
158
+ ```typescript
159
+ let db = get(Database)
160
+
161
+ // Find by primary key
162
+ let book = await db.find(books, id)
163
+
164
+ // Find one by condition
165
+ let user = await db.findOne(users, { where: { email } })
166
+
167
+ // Find many with ordering
168
+ let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
169
+
170
+ // Count
171
+ let total = await db.count(orders, { where: { user_id: userId } })
172
+
173
+ // Query builder
174
+ let genres = await db.query(books).select('genre').distinct().orderBy('genre', 'asc').all()
175
+
176
+ // Create
177
+ let newBook = await db.create(books, { slug: 'new-book', title: 'New Book' /* ... */ })
178
+
179
+ // Update
180
+ await db.update(books, bookId, { title: 'Updated Title' })
181
+
182
+ // Delete
183
+ await db.delete(books, bookId)
184
+ ```
185
+
186
+ ### Operators
187
+
188
+ ```typescript
189
+ import { inList } from 'remix/data-table/operators'
190
+
191
+ let featured = await db.findMany(books, {
192
+ where: inList('slug', ['book-a', 'book-b', 'book-c']),
193
+ })
194
+ ```
195
+
196
+ ## Migrations
197
+
198
+ ### Writing migrations
199
+
200
+ ```typescript
201
+ import { column as c, createMigration } from 'remix/data-table/migrations'
202
+ import { table } from 'remix/data-table'
203
+
204
+ export default createMigration({
205
+ async up({ schema }) {
206
+ let users = table({
207
+ name: 'users',
208
+ columns: {
209
+ id: c.integer().primaryKey().autoIncrement(),
210
+ email: c.text().notNull().unique(),
211
+ name: c.text().notNull(),
212
+ },
213
+ })
214
+ await schema.createTable(users)
215
+ await schema.createIndex(users, 'email', { name: 'users_email_idx', unique: true })
216
+ },
217
+
218
+ async down({ schema }) {
219
+ await schema.dropTable('users')
220
+ },
221
+ })
222
+ ```
223
+
224
+ Migrations can also import table definitions from the app schema to avoid duplication:
225
+
226
+ ```typescript
227
+ import { createMigration } from 'remix/data-table/migrations'
228
+ import { users, authAccounts } from '../../app/data/schema.ts'
229
+
230
+ export default createMigration({
231
+ async up({ schema }) {
232
+ await schema.createTable(users)
233
+ await schema.createTable(authAccounts)
234
+ },
235
+ })
236
+ ```
237
+
238
+ ### Running migrations
239
+
240
+ ```typescript
241
+ import { createMigrationRunner } from 'remix/data-table/migrations'
242
+ import { loadMigrations } from 'remix/data-table/migrations/node'
243
+
244
+ let migrations = await loadMigrations('./db/migrations')
245
+ let runner = createMigrationRunner(adapter, migrations)
246
+ await runner.up()
247
+ ```
248
+
249
+ ### Migration file naming
250
+
251
+ Name migration files with a timestamp prefix: `20260228090000_create_users.ts`. Place them in
252
+ `db/migrations/`.
253
+
254
+ ## Input Validation (`remix/data-schema`)
255
+
256
+ Use `data-schema` to validate user input (forms, query params, API payloads). This is separate from
257
+ table-level `validate` hooks which run at persistence.
258
+
259
+ ### Schema builders
260
+
261
+ ```typescript
262
+ import * as s from 'remix/data-schema'
263
+ import { email, minLength, maxLength } from 'remix/data-schema/checks'
264
+
265
+ let userSchema = s.object({
266
+ name: s.string().pipe(minLength(1)),
267
+ email: s.string().pipe(email()),
268
+ age: s.optional(s.number()),
269
+ })
270
+
271
+ let result = s.parse(userSchema, data)
272
+ ```
273
+
274
+ ### FormData validation
275
+
276
+ Use `remix/data-schema/form-data` to validate `FormData` directly:
277
+
278
+ ```typescript
279
+ import * as s from 'remix/data-schema'
280
+ import * as f from 'remix/data-schema/form-data'
281
+ import { email, minLength } from 'remix/data-schema/checks'
282
+
283
+ let signupSchema = f.object({
284
+ name: f.field(s.string().pipe(minLength(1))),
285
+ email: f.field(s.string().pipe(email())),
286
+ password: f.field(s.string().pipe(minLength(8))),
287
+ })
288
+
289
+ // In a controller action:
290
+ let formData = get(FormData)
291
+ let { name, email, password } = s.parse(signupSchema, formData)
292
+ ```
293
+
294
+ ### Reading FormData: middleware vs `request.formData()`
295
+
296
+ There are two ways to get a `FormData` value inside an action.
297
+
298
+ The recommended way: register `formData()` middleware in the root stack and read with
299
+ `get(FormData)`. The body is parsed once per request, and the typed `FormData` value flows through
300
+ the context system. This also lets `methodOverride()` and CSRF middleware work uniformly.
301
+
302
+ ```typescript
303
+ import { formData } from 'remix/middleware/form-data'
304
+
305
+ let router = createRouter({
306
+ middleware: [, /* ... */ formData() /* ... */],
307
+ })
308
+
309
+ // In an action:
310
+ let parsed = s.parseSafe(signupSchema, get(FormData))
311
+ ```
312
+
313
+ The fallback: `await request.formData()` directly. This works without middleware and is fine for
314
+ small one-off cases, but it bypasses the context system, runs once per call site, and doesn't
315
+ compose with middleware that depends on parsed form fields.
316
+
317
+ ### Safe parsing
318
+
319
+ `s.parse` throws on invalid input. `s.parseSafe` returns a tagged result and is usually what an
320
+ action wants, since validation failure is an expected outcome (re-render the form with errors)
321
+ rather than an exception:
322
+
323
+ ```typescript
324
+ let result = s.parseSafe(signupSchema, get(FormData))
325
+ if (!result.success) {
326
+ return render(<SignupPage errors={result.issues} />, { status: 400 })
327
+ }
328
+ let { name, email, password } = result.value
329
+ ```
330
+
331
+ Returning a `Response` for validation failures keeps the route contract honest: the same action
332
+ returns 200 on success, 400 with errors on bad input, no out-of-band exception flow.
333
+
334
+ ### Transforming validated output
335
+
336
+ Use `.transform(...)` when a schema should validate one shape but return another value or output
337
+ type. Transforms run after validation and compose with `.pipe(...)` and `.refine(...)`:
338
+
339
+ ```typescript
340
+ import * as coerce from 'remix/data-schema/coerce'
341
+
342
+ let slugSchema = s
343
+ .string()
344
+ .pipe(minLength(1))
345
+ .transform((value) => value.trim().toLowerCase().replace(/\s+/g, '-'))
346
+
347
+ let pageSchema = f.object({
348
+ page: f.field(s.defaulted(coerce.coerceNumber(), 1).refine(Number.isInteger)),
349
+ q: f.field(s.defaulted(s.string(), '').transform((value) => value.trim())),
350
+ })
351
+
352
+ let { page, q } = s.parse(pageSchema, formData)
353
+ ```
354
+
355
+ ### Anti-patterns
356
+
357
+ Avoid these shapes when reading and validating input:
358
+
359
+ - **Raw `formData.get('name')` plus an `if (typeof name !== 'string')` guard**, then a thrown
360
+ custom error. This reinvents what `data-schema` already does, loses the typed result, and
361
+ pushes error translation into a `try/catch` instead of a return value.
362
+ - **Letting route-local domain errors leak out of the action.** Translate expected outcomes (bad
363
+ input, missing record, duplicate entry) into the `Response` the route means to return instead of
364
+ throwing a custom `Error` subclass with a `status` field and catching it later.
365
+ - **Trusting `params`, query strings, or external payloads without a schema.** Anything that
366
+ crosses a trust boundary should be parsed before it reaches business logic.
367
+
368
+ ### Common patterns
369
+
370
+ ```typescript
371
+ // Optional with default
372
+ let limitSchema = f.field(s.defaulted(s.string(), '10'))
373
+
374
+ // Union types
375
+ let methodSchema = s.union([s.literal('credentials'), s.literal('google'), s.literal('github')])
376
+
377
+ // Refinements
378
+ let idSchema = s.number().refine(Number.isInteger, 'Expected an integer')
379
+ ```
@@ -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.