@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,282 @@
1
+ # Component Model
2
+
3
+ ## What This Covers
4
+
5
+ How a Remix Component is shaped and how its state, lifecycle, and updates behave. Read this when
6
+ the task involves:
7
+
8
+ - Writing a component (`handle` plus render function)
9
+ - Managing component-local state, derived values, or post-render DOM work
10
+ - Using `handle.props`, `handle.update()`, `handle.queueTask()`, `handle.signal`, `handle.id`, or
11
+ `handle.context`
12
+ - Listening to global events with cleanup tied to the component lifecycle
13
+
14
+ For host-element behavior (event handlers, styles, refs, animations), see
15
+ `mixins-styling-events.md`. For browser hydration, frames, and navigation, see
16
+ `hydration-frames-navigation.md`.
17
+
18
+ ## Phases
19
+
20
+ A component has two phases:
21
+
22
+ 1. **Setup phase** — runs once when the component is created
23
+ 2. **Render phase** — returned function runs on initial render and every update
24
+
25
+ ```tsx
26
+ import { on, type Handle } from 'remix/ui'
27
+
28
+ function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
29
+ let count = handle.props.initialCount ?? 0
30
+
31
+ return () => (
32
+ <button
33
+ mix={on('click', () => {
34
+ count++
35
+ handle.update()
36
+ })}
37
+ >
38
+ {handle.props.label}: {count}
39
+ </button>
40
+ )
41
+ }
42
+ ```
43
+
44
+ ## Props
45
+
46
+ Components receive all JSX props through `handle.props`. The object identity is stable for the
47
+ component lifetime, and its values are updated before each render. Put initialization inputs on
48
+ normal JSX props and read them from `handle.props`:
49
+
50
+ ```tsx
51
+ function Timer(handle: Handle<{ initialSeconds: number; paused?: boolean }>) {
52
+ let seconds = handle.props.initialSeconds
53
+
54
+ return () => <div>Time remaining: {seconds}s</div>
55
+ }
56
+
57
+ // Usage: <Timer initialSeconds={60} paused={false} />
58
+ ```
59
+
60
+ Because `handle.props` is stable, destructuring `let { props } = handle` is safe when helpers need
61
+ to read current values later. Destructuring individual prop values is only a snapshot; prefer
62
+ `handle.props.name` inside callbacks and render output when values can change.
63
+
64
+ ## State Rules
65
+
66
+ - Keep state in setup scope as plain JavaScript variables.
67
+ - Store only what affects rendering. Derive computed values in render.
68
+ - Do not mirror input state unless you truly need controlled behavior.
69
+ - Do work in event handlers, not in render. Use the handler scope for transient state.
70
+
71
+ ```tsx
72
+ // Derive computed values in render
73
+ function TodoList(handle: Handle) {
74
+ let todos: Array<{ text: string; completed: boolean }> = []
75
+
76
+ return () => {
77
+ let completedCount = todos.filter((t) => t.completed).length
78
+ return <div>Completed: {completedCount}</div>
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Handle API
84
+
85
+ ### `handle.update()`
86
+
87
+ Schedules a rerender. Returns a promise that resolves with an `AbortSignal` after the update
88
+ completes. Await it when you need the updated DOM before follow-up work:
89
+
90
+ ```tsx
91
+ on('click', async () => {
92
+ isPlaying = true
93
+ let signal = await handle.update()
94
+ // DOM is now updated, safe to focus or measure
95
+ stopButton.focus()
96
+ })
97
+ ```
98
+
99
+ ### `handle.queueTask(task)`
100
+
101
+ Schedules a task to run after the next update. The task receives an `AbortSignal` that aborts when
102
+ the component re-renders or is removed. Use for post-render DOM work, reactive data loading, or
103
+ hydration-sensitive setup:
104
+
105
+ ```tsx
106
+ let data = null
107
+ let requestedUrl: string | null = null
108
+
109
+ // Post-render DOM work in an event handler
110
+ on('click', () => {
111
+ showDetails = true
112
+ handle.update()
113
+ handle.queueTask(() => {
114
+ detailsSection.scrollIntoView({ behavior: 'smooth' })
115
+ })
116
+ })
117
+
118
+ // Reactive data loading keyed by props.url
119
+ return () => {
120
+ if (requestedUrl !== handle.props.url) {
121
+ let nextUrl = handle.props.url
122
+ requestedUrl = nextUrl
123
+ data = null
124
+
125
+ handle.queueTask(async (signal) => {
126
+ let response = await fetch(nextUrl, { signal })
127
+ let json = await response.json()
128
+ if (signal.aborted || requestedUrl !== nextUrl) return
129
+ data = json
130
+ handle.update()
131
+ })
132
+ }
133
+
134
+ return <div>{data ?? 'Loading...'}</div>
135
+ }
136
+ ```
137
+
138
+ Avoid creating intermediate state just to trigger `queueTask`. Do the work directly in the handler
139
+ or the queued task.
140
+
141
+ ### `handle.signal`
142
+
143
+ An `AbortSignal` aborted when the component disconnects. Use for cleanup:
144
+
145
+ ```tsx
146
+ function Clock(handle: Handle) {
147
+ let interval = setInterval(handle.update, 1000)
148
+ handle.signal.addEventListener('abort', () => clearInterval(interval))
149
+
150
+ return () => <span>{new Date().toString()}</span>
151
+ }
152
+ ```
153
+
154
+ ### `handle.id`
155
+
156
+ Stable identifier per component instance. Useful for `htmlFor`, `aria-owns`, etc.:
157
+
158
+ ```tsx
159
+ function LabeledInput(handle: Handle) {
160
+ return () => (
161
+ <div>
162
+ <label htmlFor={handle.id}>Name</label>
163
+ <input id={handle.id} type="text" />
164
+ </div>
165
+ )
166
+ }
167
+ ```
168
+
169
+ ### `handle.frame` and `handle.frames`
170
+
171
+ Frame-aware behavior for client entries rendered inside frames:
172
+
173
+ - `handle.frame.reload()` — reload the containing frame
174
+ - `handle.frame.src` — the URL of the containing frame
175
+ - `handle.frames.top` — the root frame (the whole page)
176
+ - `handle.frames.top.reload()` — reload the entire page/frame tree
177
+ - `handle.frames.get(name)` — look up a named frame; returns `FrameHandle | undefined`
178
+
179
+ ```tsx
180
+ function RefreshButton(handle: Handle) {
181
+ return () => <button mix={on('click', () => handle.frame.reload())}>Refresh</button>
182
+ }
183
+ ```
184
+
185
+ ### `handle.context`
186
+
187
+ Context for ancestor/descendant communication. See the context section below.
188
+
189
+ ## Context
190
+
191
+ Use `handle.context.set()` to provide values and `handle.context.get(Provider)` to consume them.
192
+ `set()` does **not** trigger updates — call `handle.update()` if the tree needs to rerender.
193
+
194
+ ```tsx
195
+ function ThemeProvider(handle: Handle<{ children?: RemixNode }, { theme: 'light' | 'dark' }>) {
196
+ let theme: 'light' | 'dark' = 'light'
197
+ handle.context.set({ theme })
198
+
199
+ return () => (
200
+ <div>
201
+ <button
202
+ mix={on('click', () => {
203
+ theme = theme === 'light' ? 'dark' : 'light'
204
+ handle.context.set({ theme })
205
+ handle.update()
206
+ })}
207
+ >
208
+ Toggle
209
+ </button>
210
+ {handle.props.children}
211
+ </div>
212
+ )
213
+ }
214
+
215
+ function ThemedContent(handle: Handle) {
216
+ let { theme } = handle.context.get(ThemeProvider)
217
+ return () => <div>Current theme: {theme}</div>
218
+ }
219
+ ```
220
+
221
+ For granular updates without re-rendering the full subtree, use `TypedEventTarget`:
222
+
223
+ ```tsx
224
+ import { TypedEventTarget, addEventListeners } from 'remix/ui'
225
+
226
+ class Theme extends TypedEventTarget<{ change: Event }> {
227
+ #value: 'light' | 'dark' = 'light'
228
+ get value() {
229
+ return this.#value
230
+ }
231
+ setValue(value: 'light' | 'dark') {
232
+ this.#value = value
233
+ this.dispatchEvent(new Event('change'))
234
+ }
235
+ }
236
+
237
+ function ThemeProvider(handle: Handle<{ children?: RemixNode }, Theme>) {
238
+ let theme = new Theme()
239
+ handle.context.set(theme)
240
+
241
+ return () => (
242
+ <div>
243
+ <button mix={on('click', () => theme.setValue(theme.value === 'light' ? 'dark' : 'light'))}>
244
+ Toggle
245
+ </button>
246
+ {handle.props.children}
247
+ </div>
248
+ )
249
+ }
250
+
251
+ function ThemedContent(handle: Handle) {
252
+ let theme = handle.context.get(ThemeProvider)
253
+ addEventListeners(theme, handle.signal, {
254
+ change() {
255
+ handle.update()
256
+ },
257
+ })
258
+ return () => <div>Theme: {theme.value}</div>
259
+ }
260
+ ```
261
+
262
+ ## Global Events
263
+
264
+ Use `addEventListeners(target, handle.signal, listeners)` to listen to global targets with
265
+ automatic cleanup when the component disconnects:
266
+
267
+ ```tsx
268
+ import { addEventListeners, type Handle } from 'remix/ui'
269
+
270
+ function ResizeTracker(handle: Handle) {
271
+ let width = window.innerWidth
272
+
273
+ addEventListeners(window, handle.signal, {
274
+ resize() {
275
+ width = window.innerWidth
276
+ handle.update()
277
+ },
278
+ })
279
+
280
+ return () => <div>{width}</div>
281
+ }
282
+ ```
@@ -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
+ ```