@remix-run/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +0 -3
  2. package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
  3. package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
  4. package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
  5. package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
  6. package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
  7. package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
  8. package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
  9. package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  10. package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
  11. package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  12. package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
  13. package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
  14. package/bootstrap/AGENTS.md +4 -0
  15. package/bootstrap/app/assets/entry.ts +19 -0
  16. package/bootstrap/app/assets.ts +18 -0
  17. package/bootstrap/app/controllers/auth.tsx +2 -2
  18. package/bootstrap/app/controllers/home.tsx +3 -18
  19. package/bootstrap/app/router.ts +6 -0
  20. package/bootstrap/app/routes.ts +2 -1
  21. package/bootstrap/app/ui/document.tsx +6 -1
  22. package/bootstrap/app/ui/prompt-button.tsx +162 -0
  23. package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
  24. package/bootstrap/app/utils/render.tsx +22 -3
  25. package/bootstrap/server.ts +13 -13
  26. package/bootstrap/tsconfig.json +0 -1
  27. package/dist/lib/cli.d.ts.map +1 -1
  28. package/dist/lib/cli.js +7 -10
  29. package/dist/lib/commands/help.d.ts.map +1 -1
  30. package/dist/lib/commands/help.js +9 -33
  31. package/dist/lib/commands/test.d.ts +1 -1
  32. package/dist/lib/commands/test.d.ts.map +1 -1
  33. package/dist/lib/commands/test.js +8 -4
  34. package/dist/lib/completion.d.ts.map +1 -1
  35. package/dist/lib/completion.js +3 -101
  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/package.json +3 -4
  40. package/src/lib/cli.ts +7 -11
  41. package/src/lib/commands/help.ts +9 -43
  42. package/src/lib/commands/test.ts +10 -4
  43. package/src/lib/completion.ts +3 -146
  44. package/src/lib/errors.ts +0 -12
  45. package/dist/lib/commands/skills.d.ts +0 -6
  46. package/dist/lib/commands/skills.d.ts.map +0 -1
  47. package/dist/lib/commands/skills.js +0 -222
  48. package/dist/lib/skills-cache.d.ts +0 -19
  49. package/dist/lib/skills-cache.d.ts.map +0 -1
  50. package/dist/lib/skills-cache.js +0 -89
  51. package/dist/lib/skills.d.ts +0 -30
  52. package/dist/lib/skills.d.ts.map +0 -1
  53. package/dist/lib/skills.js +0 -441
  54. package/src/lib/commands/skills.ts +0 -306
  55. package/src/lib/skills-cache.ts +0 -140
  56. package/src/lib/skills.ts +0 -706
@@ -0,0 +1,158 @@
1
+ # Creating Mixins
2
+
3
+ ## What This Covers
4
+
5
+ How to author your own reusable host-element behavior with `createMixin`. Read this when the task
6
+ involves:
7
+
8
+ - Combining multiple low-level events or DOM hooks into one semantic mixin
9
+ - Dispatching custom DOM events from a host node
10
+ - Encapsulating imperative DOM setup that several components share
11
+ - Typing custom events on `HTMLElementEventMap` for use with `on(...)`
12
+
13
+ For the built-in mixins most code should use, see `mixins-styling-events.md`.
14
+
15
+ Use `createMixin` from `remix/ui` to author reusable host-element behavior.
16
+
17
+ Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`, `attrs`) and animation
18
+ mixins from `remix/ui/animation`. Create custom mixins when combining multiple low-level events
19
+ into one semantic event, or when the pattern is reused across components.
20
+
21
+ ## Core Semantics
22
+
23
+ 1. A mixin handle is tied to one mounted host node lifecycle.
24
+ 2. `insert` is the host-node availability point for imperative setup.
25
+ 3. `remove` is teardown for that same lifecycle.
26
+ 4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.
27
+ 5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued
28
+ work.
29
+
30
+ ```tsx
31
+ import { createMixin } from 'remix/ui'
32
+
33
+ let myMixin = createMixin<HTMLElement>((handle) => {
34
+ handle.addEventListener('insert', (event) => {
35
+ // event.node is the mounted host node
36
+ })
37
+
38
+ handle.addEventListener('remove', () => {
39
+ // Clean up listeners, timers, observers
40
+ })
41
+
42
+ return (props) => {
43
+ handle.queueTask((node) => {
44
+ // Post-commit work that needs the concrete host node
45
+ })
46
+ return <handle.element {...props} />
47
+ }
48
+ })
49
+ ```
50
+
51
+ ## Patterns
52
+
53
+ ### Pure prop transform
54
+
55
+ ```tsx
56
+ let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
57
+ <handle.element {...props} title={title} />
58
+ ))
59
+ ```
60
+
61
+ ### Lifecycle-managed imperative setup
62
+
63
+ ```tsx
64
+ let withFocus = createMixin<HTMLElement>((handle) => {
65
+ handle.addEventListener('insert', (event) => {
66
+ event.node.focus()
67
+ })
68
+ return (props) => <handle.element {...props} />
69
+ })
70
+ ```
71
+
72
+ ## Custom Event Mixins
73
+
74
+ Create event mixins when you combine multiple low-level events into one semantic custom event that
75
+ is reused across components.
76
+
77
+ 1. Namespace custom event names (`myapp:*`) to avoid collisions.
78
+ 2. Extend `Event` with the data consumers need.
79
+ 3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`.
80
+ 4. Dispatch from the host node inside the mixin.
81
+
82
+ ```tsx
83
+ import { createMixin, on } from 'remix/ui'
84
+
85
+ export let dragReleaseType = 'myapp:drag-release' as const
86
+
87
+ declare global {
88
+ interface HTMLElementEventMap {
89
+ [dragReleaseType]: DragReleaseEvent
90
+ }
91
+ }
92
+
93
+ export class DragReleaseEvent extends Event {
94
+ velocityX: number
95
+ velocityY: number
96
+ constructor(init: { velocityX: number; velocityY: number }) {
97
+ super(dragReleaseType, { bubbles: true, cancelable: true })
98
+ this.velocityX = init.velocityX
99
+ this.velocityY = init.velocityY
100
+ }
101
+ }
102
+
103
+ export let dragRelease = createMixin<HTMLElement>((handle) => {
104
+ let node: HTMLElement | undefined
105
+ let tracking = false
106
+ let velocityX = 0
107
+ let velocityY = 0
108
+ let lastX = 0
109
+ let lastY = 0
110
+ let lastT = 0
111
+
112
+ handle.addEventListener('insert', (event) => {
113
+ node = event.node
114
+ })
115
+
116
+ return () => (
117
+ <handle.element
118
+ mix={[
119
+ on('pointerdown', (event) => {
120
+ if (!event.isPrimary) return
121
+ tracking = true
122
+ lastX = event.clientX
123
+ lastY = event.clientY
124
+ lastT = event.timeStamp
125
+ node?.setPointerCapture(event.pointerId)
126
+ }),
127
+ on('pointermove', (event) => {
128
+ if (!tracking) return
129
+ let dt = Math.max(1, event.timeStamp - lastT)
130
+ velocityX = (event.clientX - lastX) / dt
131
+ velocityY = (event.clientY - lastY) / dt
132
+ lastX = event.clientX
133
+ lastY = event.clientY
134
+ lastT = event.timeStamp
135
+ }),
136
+ on('pointerup', () => {
137
+ if (!tracking) return
138
+ tracking = false
139
+ node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
140
+ }),
141
+ ]}
142
+ />
143
+ )
144
+ })
145
+ ```
146
+
147
+ Consume it:
148
+
149
+ ```tsx
150
+ <div
151
+ mix={[
152
+ dragRelease(),
153
+ on(dragReleaseType, (event) => {
154
+ console.log('velocity:', event.velocityX, event.velocityY)
155
+ }),
156
+ ]}
157
+ />
158
+ ```
@@ -0,0 +1,363 @@
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 validation hooks
88
+
89
+ Tables can define `validate`, `beforeWrite`, and `afterRead` hooks:
90
+
91
+ ```typescript
92
+ export const books = table({
93
+ name: 'books',
94
+ columns: {
95
+ /* ... */
96
+ },
97
+ validate({ operation, value }) {
98
+ let issues = []
99
+ if (operation === 'create' && !value.slug) {
100
+ issues.push({ message: 'Slug is required.', path: ['slug'] })
101
+ }
102
+ return issues.length > 0 ? { issues } : { value }
103
+ },
104
+ })
105
+ ```
106
+
107
+ ## Database Setup
108
+
109
+ Create a database with an adapter and expose it via middleware:
110
+
111
+ ```typescript
112
+ import BetterSqlite3 from 'better-sqlite3'
113
+ import { createDatabase, Database } from 'remix/data-table'
114
+ import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
115
+
116
+ let sqlite = new BetterSqlite3('./db/app.db')
117
+ sqlite.pragma('foreign_keys = ON')
118
+ let adapter = createSqliteDatabaseAdapter(sqlite)
119
+ export let db = createDatabase(adapter)
120
+ ```
121
+
122
+ `createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec`
123
+ surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever
124
+ client fits the runtime instead of assuming `better-sqlite3` is required.
125
+
126
+ ### Database middleware
127
+
128
+ ```typescript
129
+ import type { Middleware } from 'remix/fetch-router'
130
+ import { Database } from 'remix/data-table'
131
+
132
+ export function loadDatabase(): Middleware {
133
+ return async (context, next) => {
134
+ context.set(Database, db)
135
+ return next()
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Querying
141
+
142
+ ```typescript
143
+ let db = get(Database)
144
+
145
+ // Find by primary key
146
+ let book = await db.find(books, id)
147
+
148
+ // Find one by condition
149
+ let user = await db.findOne(users, { where: { email } })
150
+
151
+ // Find many with ordering
152
+ let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
153
+
154
+ // Count
155
+ let total = await db.count(orders, { where: { user_id: userId } })
156
+
157
+ // Query builder
158
+ let genres = await db.query(books).select('genre').distinct().orderBy('genre', 'asc').all()
159
+
160
+ // Create
161
+ let newBook = await db.create(books, { slug: 'new-book', title: 'New Book' /* ... */ })
162
+
163
+ // Update
164
+ await db.update(books, bookId, { title: 'Updated Title' })
165
+
166
+ // Delete
167
+ await db.delete(books, bookId)
168
+ ```
169
+
170
+ ### Operators
171
+
172
+ ```typescript
173
+ import { inList } from 'remix/data-table/operators'
174
+
175
+ let featured = await db.findMany(books, {
176
+ where: inList('slug', ['book-a', 'book-b', 'book-c']),
177
+ })
178
+ ```
179
+
180
+ ## Migrations
181
+
182
+ ### Writing migrations
183
+
184
+ ```typescript
185
+ import { column as c, createMigration } from 'remix/data-table/migrations'
186
+ import { table } from 'remix/data-table'
187
+
188
+ export default createMigration({
189
+ async up({ schema }) {
190
+ let users = table({
191
+ name: 'users',
192
+ columns: {
193
+ id: c.integer().primaryKey().autoIncrement(),
194
+ email: c.text().notNull().unique(),
195
+ name: c.text().notNull(),
196
+ },
197
+ })
198
+ await schema.createTable(users)
199
+ await schema.createIndex(users, 'email', { name: 'users_email_idx', unique: true })
200
+ },
201
+
202
+ async down({ schema }) {
203
+ await schema.dropTable('users')
204
+ },
205
+ })
206
+ ```
207
+
208
+ Migrations can also import table definitions from the app schema to avoid duplication:
209
+
210
+ ```typescript
211
+ import { createMigration } from 'remix/data-table/migrations'
212
+ import { users, authAccounts } from '../../app/data/schema.ts'
213
+
214
+ export default createMigration({
215
+ async up({ schema }) {
216
+ await schema.createTable(users)
217
+ await schema.createTable(authAccounts)
218
+ },
219
+ })
220
+ ```
221
+
222
+ ### Running migrations
223
+
224
+ ```typescript
225
+ import { createMigrationRunner } from 'remix/data-table/migrations'
226
+ import { loadMigrations } from 'remix/data-table/migrations/node'
227
+
228
+ let migrations = await loadMigrations('./db/migrations')
229
+ let runner = createMigrationRunner(adapter, migrations)
230
+ await runner.up()
231
+ ```
232
+
233
+ ### Migration file naming
234
+
235
+ Name migration files with a timestamp prefix: `20260228090000_create_users.ts`. Place them in
236
+ `db/migrations/`.
237
+
238
+ ## Input Validation (`remix/data-schema`)
239
+
240
+ Use `data-schema` to validate user input (forms, query params, API payloads). This is separate from
241
+ table-level `validate` hooks which run at persistence.
242
+
243
+ ### Schema builders
244
+
245
+ ```typescript
246
+ import * as s from 'remix/data-schema'
247
+ import { email, minLength, maxLength } from 'remix/data-schema/checks'
248
+
249
+ let userSchema = s.object({
250
+ name: s.string().pipe(minLength(1)),
251
+ email: s.string().pipe(email()),
252
+ age: s.optional(s.number()),
253
+ })
254
+
255
+ let result = s.parse(userSchema, data)
256
+ ```
257
+
258
+ ### FormData validation
259
+
260
+ Use `remix/data-schema/form-data` to validate `FormData` directly:
261
+
262
+ ```typescript
263
+ import * as s from 'remix/data-schema'
264
+ import * as f from 'remix/data-schema/form-data'
265
+ import { email, minLength } from 'remix/data-schema/checks'
266
+
267
+ let signupSchema = f.object({
268
+ name: f.field(s.string().pipe(minLength(1))),
269
+ email: f.field(s.string().pipe(email())),
270
+ password: f.field(s.string().pipe(minLength(8))),
271
+ })
272
+
273
+ // In a controller action:
274
+ let formData = get(FormData)
275
+ let { name, email, password } = s.parse(signupSchema, formData)
276
+ ```
277
+
278
+ ### Reading FormData: middleware vs `request.formData()`
279
+
280
+ There are two ways to get a `FormData` value inside an action.
281
+
282
+ The recommended way: register `formData()` middleware in the root stack and read with
283
+ `get(FormData)`. The body is parsed once per request, and the typed `FormData` value flows through
284
+ the context system. This also lets `methodOverride()` and CSRF middleware work uniformly.
285
+
286
+ ```typescript
287
+ import { formData } from 'remix/form-data-middleware'
288
+
289
+ let router = createRouter({
290
+ middleware: [, /* ... */ formData() /* ... */],
291
+ })
292
+
293
+ // In an action:
294
+ let parsed = s.parseSafe(signupSchema, get(FormData))
295
+ ```
296
+
297
+ The fallback: `await request.formData()` directly. This works without middleware and is fine for
298
+ small one-off cases, but it bypasses the context system, runs once per call site, and doesn't
299
+ compose with middleware that depends on parsed form fields.
300
+
301
+ ### Safe parsing
302
+
303
+ `s.parse` throws on invalid input. `s.parseSafe` returns a tagged result and is usually what an
304
+ action wants, since validation failure is an expected outcome (re-render the form with errors)
305
+ rather than an exception:
306
+
307
+ ```typescript
308
+ let result = s.parseSafe(signupSchema, get(FormData))
309
+ if (!result.success) {
310
+ return render(<SignupPage errors={result.issues} />, { status: 400 })
311
+ }
312
+ let { name, email, password } = result.value
313
+ ```
314
+
315
+ Returning a `Response` for validation failures keeps the route contract honest: the same action
316
+ returns 200 on success, 400 with errors on bad input, no out-of-band exception flow.
317
+
318
+ ### Transforming validated output
319
+
320
+ Use `.transform(...)` when a schema should validate one shape but return another value or output
321
+ type. Transforms run after validation and compose with `.pipe(...)` and `.refine(...)`:
322
+
323
+ ```typescript
324
+ import * as coerce from 'remix/data-schema/coerce'
325
+
326
+ let slugSchema = s
327
+ .string()
328
+ .pipe(minLength(1))
329
+ .transform((value) => value.trim().toLowerCase().replace(/\s+/g, '-'))
330
+
331
+ let pageSchema = f.object({
332
+ page: f.field(s.defaulted(coerce.coerceNumber(), 1).refine(Number.isInteger)),
333
+ q: f.field(s.defaulted(s.string(), '').transform((value) => value.trim())),
334
+ })
335
+
336
+ let { page, q } = s.parse(pageSchema, formData)
337
+ ```
338
+
339
+ ### Anti-patterns
340
+
341
+ Avoid these shapes when reading and validating input:
342
+
343
+ - **Raw `formData.get('name')` plus an `if (typeof name !== 'string')` guard**, then a thrown
344
+ custom error. This reinvents what `data-schema` already does, loses the typed result, and
345
+ pushes error translation into a `try/catch` instead of a return value.
346
+ - **Letting route-local domain errors leak out of the action.** Translate expected outcomes (bad
347
+ input, missing record, duplicate entry) into the `Response` the route means to return instead of
348
+ throwing a custom `Error` subclass with a `status` field and catching it later.
349
+ - **Trusting `params`, query strings, or external payloads without a schema.** Anything that
350
+ crosses a trust boundary should be parsed before it reaches business logic.
351
+
352
+ ### Common patterns
353
+
354
+ ```typescript
355
+ // Optional with default
356
+ let limitSchema = f.field(s.defaulted(s.string(), '10'))
357
+
358
+ // Union types
359
+ let methodSchema = s.union([s.literal('credentials'), s.literal('google'), s.literal('github')])
360
+
361
+ // Refinements
362
+ let idSchema = s.number().refine(Number.isInteger, 'Expected an integer')
363
+ ```