@remix-run/cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/bootstrap-project.js +7 -4
- package/dist/lib/help-text.js +1 -1
- package/package.json +4 -4
- package/src/lib/bootstrap-project.ts +9 -6
- package/src/lib/help-text.ts +3 -2
- package/template/.agents/skills/remix/SKILL.md +115 -264
- package/template/.agents/skills/remix/references/animate-elements.md +8 -17
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +14 -33
- package/template/.agents/skills/remix/references/auth-and-sessions.md +10 -31
- package/template/.agents/skills/remix/references/component-model.md +13 -25
- package/template/.agents/skills/remix/references/create-mixins.md +4 -9
- package/template/.agents/skills/remix/references/data-and-validation.md +54 -77
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +23 -56
- package/template/.agents/skills/remix/references/middleware-and-server.md +17 -39
- package/template/.agents/skills/remix/references/mixins-styling-events.md +14 -32
- package/template/.agents/skills/remix/references/routing-and-controllers.md +17 -43
- package/template/.agents/skills/remix/references/testing-patterns.md +13 -32
- package/template/app/middleware/render.tsx +2 -1
- package/template/app/ui/document.tsx +22 -17
- package/template/app/ui/scaffold-home-page.tsx +56 -47
- package/template/server.ts +3 -1
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How to animate insertion, removal, and layout changes of elements. Read this when the task
|
|
6
|
-
involves:
|
|
5
|
+
How to animate insertion, removal, and layout changes of elements. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Adding entrance, exit, or shared-layout transitions to UI
|
|
9
8
|
- Choosing between spring physics (`spring(...)`) and time-based easing (`tween`)
|
|
10
9
|
- Coordinating CSS transitions with the same easing as JS animations
|
|
11
10
|
- Imperative animation loops via `requestAnimationFrame`
|
|
12
11
|
|
|
13
|
-
Import animation APIs from `remix/ui/animation`. For the smaller set of animation helpers that
|
|
14
|
-
show up alongside other mixins, see `mixins-styling-events.md`.
|
|
12
|
+
Import animation APIs from `remix/ui/animation`. For the smaller set of animation helpers that show up alongside other mixins, see `mixins-styling-events.md`.
|
|
15
13
|
|
|
16
14
|
## Animation Mixins
|
|
17
15
|
|
|
18
16
|
### `animateEntrance(config)`
|
|
19
17
|
|
|
20
|
-
Animates an element when inserted. Config specifies the **starting** style the element animates
|
|
21
|
-
**from**:
|
|
18
|
+
Animates an element when inserted. Config specifies the **starting** style the element animates **from**:
|
|
22
19
|
|
|
23
20
|
```tsx
|
|
24
21
|
<div
|
|
@@ -32,8 +29,7 @@ Animates an element when inserted. Config specifies the **starting** style the e
|
|
|
32
29
|
|
|
33
30
|
### `animateExit(config)`
|
|
34
31
|
|
|
35
|
-
Animates an element when removed. Config specifies the **ending** style the element animates
|
|
36
|
-
**to**. The element stays in the DOM until the animation completes:
|
|
32
|
+
Animates an element when removed. Config specifies the **ending** style the element animates **to**. The element stays in the DOM until the animation completes:
|
|
37
33
|
|
|
38
34
|
```tsx
|
|
39
35
|
{
|
|
@@ -61,8 +57,7 @@ Animates layout changes (position/size) using FLIP-style transforms:
|
|
|
61
57
|
}
|
|
62
58
|
```
|
|
63
59
|
|
|
64
|
-
Options: `duration` (default 200ms), `easing` (default spring snappy), `size` (default true —
|
|
65
|
-
include scale projection for size changes).
|
|
60
|
+
Options: `duration` (default 200ms), `easing` (default spring snappy), `size` (default true — include scale projection for size changes).
|
|
66
61
|
|
|
67
62
|
### Combining mixins
|
|
68
63
|
|
|
@@ -91,8 +86,7 @@ include scale projection for size changes).
|
|
|
91
86
|
|
|
92
87
|
## Spring API
|
|
93
88
|
|
|
94
|
-
Physics-based spring animation. Returns a `SpringIterator` with `duration`, `easing`, and
|
|
95
|
-
`toString()` for CSS.
|
|
89
|
+
Physics-based spring animation. Returns a `SpringIterator` with `duration`, `easing`, and `toString()` for CSS.
|
|
96
90
|
|
|
97
91
|
### Presets
|
|
98
92
|
|
|
@@ -159,9 +153,7 @@ for (let t of spring('bouncy')) {
|
|
|
159
153
|
|
|
160
154
|
## Tween API
|
|
161
155
|
|
|
162
|
-
Generator-based tween for animating values over time with cubic bezier easing. Prefer animation
|
|
163
|
-
mixins or CSS transitions with `spring` for most UI work. Use `tween` for imperative
|
|
164
|
-
`requestAnimationFrame` loops, canvas/WebGL, or non-CSS properties.
|
|
156
|
+
Generator-based tween for animating values over time with cubic bezier easing. Prefer animation mixins or CSS transitions with `spring` for most UI work. Use `tween` for imperative `requestAnimationFrame` loops, canvas/WebGL, or non-CSS properties.
|
|
165
157
|
|
|
166
158
|
```tsx
|
|
167
159
|
import { tween, easings } from 'remix/ui/animation'
|
|
@@ -183,8 +175,7 @@ function tick(timestamp: number) {
|
|
|
183
175
|
requestAnimationFrame(tick)
|
|
184
176
|
```
|
|
185
177
|
|
|
186
|
-
Built-in easings: `easings.linear`, `easings.ease`, `easings.easeIn`, `easings.easeOut`,
|
|
187
|
-
`easings.easeInOut`.
|
|
178
|
+
Built-in easings: `easings.linear`, `easings.ease`, `easings.easeIn`, `easings.easeOut`, `easings.easeInOut`.
|
|
188
179
|
|
|
189
180
|
## Practical Guidance
|
|
190
181
|
|
|
@@ -4,25 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
How to serve browser scripts and styles from source. Read this when the task involves:
|
|
6
6
|
|
|
7
|
-
- Configuring `createAssetServer` (`basePath`, `fileMap`, `allow`, `deny`, fingerprinting,
|
|
8
|
-
|
|
9
|
-
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source
|
|
10
|
-
assets that need import rewriting, preloads, or fingerprinted URLs
|
|
7
|
+
- Configuring `createAssetServer` (`basePath`, `fileMap`, `allow`, `deny`, fingerprinting, compiler options)
|
|
8
|
+
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs
|
|
11
9
|
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
|
|
12
10
|
- Keeping server-only files out of the browser via `deny` rules
|
|
13
11
|
|
|
14
|
-
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry
|
|
15
|
-
hydration, see `hydration-frames-navigation.md`.
|
|
12
|
+
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration, see `hydration-frames-navigation.md`.
|
|
16
13
|
|
|
17
14
|
## When To Reach For It
|
|
18
15
|
|
|
19
|
-
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files.
|
|
20
|
-
This is the right tool for client entrypoints, browser-only helpers, styles under `app/assets/`,
|
|
21
|
-
and monorepo code that should be compiled and served under a public URL namespace.
|
|
16
|
+
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles under `app/assets/`, and monorepo code that should be compiled and served under a public URL namespace.
|
|
22
17
|
|
|
23
|
-
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use
|
|
24
|
-
`createAssetServer()` for source scripts or styles that need rewriting, dependency scanning,
|
|
25
|
-
preloads, sourcemaps, or fingerprinted URLs.
|
|
18
|
+
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
|
|
26
19
|
|
|
27
20
|
## Default Pattern
|
|
28
21
|
|
|
@@ -66,22 +59,16 @@ export default createController(routes, {
|
|
|
66
59
|
## Rules
|
|
67
60
|
|
|
68
61
|
- Treat `allow` and `deny` as the security boundary for browser-reachable source files.
|
|
69
|
-
- Add a `deny` list for server-only modules such as `*.server.*`, private config, or other files
|
|
70
|
-
that should never be exposed.
|
|
62
|
+
- Add a `deny` list for server-only modules such as `*.server.*`, private config, or other files that should never be exposed.
|
|
71
63
|
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
|
|
72
64
|
- `basePath` is the public URL namespace handled by the asset server.
|
|
73
|
-
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
source files back to public URLs.
|
|
77
|
-
- CSS files are compiled and served alongside scripts. Local CSS `@import` rules are rewritten and
|
|
78
|
-
fingerprinted with the same asset server routing rules.
|
|
65
|
+
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides.
|
|
66
|
+
- Keep the same wildcard params on both sides of a `fileMap` entry so import rewriting can map source files back to public URLs.
|
|
67
|
+
- CSS files are compiled and served alongside scripts. Local CSS `@import` rules are rewritten and fingerprinted with the same asset server routing rules.
|
|
79
68
|
|
|
80
69
|
## Rendering HTML
|
|
81
70
|
|
|
82
|
-
Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want
|
|
83
|
-
`<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their
|
|
84
|
-
dependencies.
|
|
71
|
+
Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want `<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their dependencies.
|
|
85
72
|
|
|
86
73
|
```typescript
|
|
87
74
|
let entryHref = await assetServer.getHref('app/assets/entry.ts')
|
|
@@ -90,10 +77,7 @@ let preloads = await assetServer.getPreloads(['app/assets/entry.ts'])
|
|
|
90
77
|
|
|
91
78
|
Use this when rendering documents or layouts that boot browser behavior with a known client entry.
|
|
92
79
|
|
|
93
|
-
When resolving hydrated client entries during server rendering, pass the source entry ID from
|
|
94
|
-
`clientEntry(import.meta.url, ...)` to `getHref()` inside `resolveClientEntry`. Keep export-name
|
|
95
|
-
resolution in that render helper, and avoid hard-coding public asset URLs in source-owned component
|
|
96
|
-
modules.
|
|
80
|
+
When resolving hydrated client entries during server rendering, pass the source entry ID from `clientEntry(import.meta.url, ...)` to `getHref()` inside `resolveClientEntry`. Keep export-name resolution in that render helper, and avoid hard-coding public asset URLs in source-owned component modules.
|
|
97
81
|
|
|
98
82
|
## Development vs Deployment
|
|
99
83
|
|
|
@@ -116,15 +100,12 @@ Fingerprinting assumes files on disk are stable and requires `watch: false`.
|
|
|
116
100
|
- `minify` for production minification of scripts and styles
|
|
117
101
|
- `sourceMaps` for `'external'` or `'inline'` source maps for scripts and styles
|
|
118
102
|
- `sourceMapSourcePaths` for `'url'` or `'absolute'` source map paths
|
|
119
|
-
- `target` as an object for shared browser targets and script-only ECMAScript output, such as
|
|
120
|
-
`{ es: '2020', chrome: '109', safari: '16.4' }`
|
|
103
|
+
- `target` as an object for shared browser targets and script-only ECMAScript output, such as `{ es: '2020', chrome: '109', safari: '16.4' }`
|
|
121
104
|
- `scripts.define` to replace globals such as `process.env.NODE_ENV`
|
|
122
105
|
- `scripts.external` to leave specific script imports untouched
|
|
123
106
|
|
|
124
|
-
Do not nest shared compiler options under `scripts`. Use top-level `minify`, `sourceMaps`,
|
|
125
|
-
`sourceMapSourcePaths`, and `target` so they apply to styles as well as scripts.
|
|
107
|
+
Do not nest shared compiler options under `scripts`. Use top-level `minify`, `sourceMaps`, `sourceMapSourcePaths`, and `target` so they apply to styles as well as scripts.
|
|
126
108
|
|
|
127
109
|
## Lifecycle
|
|
128
110
|
|
|
129
|
-
If the asset server is long-lived and watching the file system, call `await assetServer.close()`
|
|
130
|
-
when shutting down dev servers or disposing tests.
|
|
111
|
+
If the asset server is long-lived and watching the file system, call `await assetServer.close()` when shutting down dev servers or disposing tests.
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How to remember things about a browser between requests and how to identify a user. Read this when
|
|
6
|
-
the task involves:
|
|
5
|
+
How to remember things about a browser between requests and how to identify a user. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Storing per-browser state across requests (login, cart, "I have submitted this form")
|
|
9
8
|
- Adding a credentials login flow or an OAuth provider
|
|
@@ -11,23 +10,15 @@ the task involves:
|
|
|
11
10
|
- Reading or writing `Session`, `Auth`, or other identity-related context values
|
|
12
11
|
- Logging in, logging out, or rotating session IDs
|
|
13
12
|
|
|
14
|
-
For raw cookies that are not session-backed (theme, locale, dismissed-banner), see
|
|
15
|
-
`createCookie` in this file plus the broader `Package Map` in `SKILL.md`.
|
|
13
|
+
For raw cookies that are not session-backed (theme, locale, dismissed-banner), see `createCookie` in this file plus the broader `Package Map` in `SKILL.md`.
|
|
16
14
|
|
|
17
15
|
## Sessions vs Plain Cookies
|
|
18
16
|
|
|
19
|
-
Reach for `remix/session` when state is sensitive, must be tamper-resistant, or represents the
|
|
20
|
-
identity of a request: who is logged in, which form a browser already submitted, what items are in
|
|
21
|
-
a cart. Sessions sign or encrypt their backing cookie with a server-held secret and give you a
|
|
22
|
-
typed `Session` object you can `get`, `set`, `flash`, `unset`, and `regenerateId`.
|
|
17
|
+
Reach for `remix/session` when state is sensitive, must be tamper-resistant, or represents the identity of a request: who is logged in, which form a browser already submitted, what items are in a cart. Sessions sign or encrypt their backing cookie with a server-held secret and give you a typed `Session` object you can `get`, `set`, `flash`, `unset`, and `regenerateId`.
|
|
23
18
|
|
|
24
|
-
Reach for `remix/cookie` directly when the browser is allowed to carry the value and the server
|
|
25
|
-
does not need session semantics. This often means preferences (theme, locale, dismissed banner),
|
|
26
|
-
but a signed cookie can also be fine for small low-risk values where you truly only need one
|
|
27
|
-
cookie-shaped fact and do not need `Session` helpers.
|
|
19
|
+
Reach for `remix/cookie` directly when the browser is allowed to carry the value and the server does not need session semantics. This often means preferences (theme, locale, dismissed banner), but a signed cookie can also be fine for small low-risk values where you truly only need one cookie-shaped fact and do not need `Session` helpers.
|
|
28
20
|
|
|
29
|
-
If a malicious user editing the value would be a bug, or if the value needs server-managed
|
|
30
|
-
lifecycle, reach for a session.
|
|
21
|
+
If a malicious user editing the value would be a bug, or if the value needs server-managed lifecycle, reach for a session.
|
|
31
22
|
|
|
32
23
|
### Quick chooser
|
|
33
24
|
|
|
@@ -60,9 +51,7 @@ export let sessionCookie = createCookie('session', {
|
|
|
60
51
|
})
|
|
61
52
|
```
|
|
62
53
|
|
|
63
|
-
The cookie should always be `httpOnly`, default to `sameSite: 'Lax'`, and be `secure` in
|
|
64
|
-
production. Demo defaults like `'s3cr3t'` are fine in tests but should never reach production —
|
|
65
|
-
fail fast when the secret is missing.
|
|
54
|
+
The cookie should always be `httpOnly`, default to `sameSite: 'Lax'`, and be `secure` in production. Demo defaults like `'s3cr3t'` are fine in tests but should never reach production — fail fast when the secret is missing.
|
|
66
55
|
|
|
67
56
|
### Create session storage
|
|
68
57
|
|
|
@@ -117,9 +106,7 @@ async function handler({ get }) {
|
|
|
117
106
|
|
|
118
107
|
### Sessions for non-auth state
|
|
119
108
|
|
|
120
|
-
Sessions are not just for login. They are the right place to store any tamper-sensitive
|
|
121
|
-
per-browser fact: which form a browser already submitted, how many free actions are left in a
|
|
122
|
-
trial, which feature flags a tester opted into, what items are in a cart.
|
|
109
|
+
Sessions are not just for login. They are the right place to store any tamper-sensitive per-browser fact: which form a browser already submitted, how many free actions are left in a trial, which feature flags a tester opted into, what items are in a cart.
|
|
123
110
|
|
|
124
111
|
```typescript
|
|
125
112
|
async function submit({ get }) {
|
|
@@ -141,10 +128,7 @@ async function submit({ get }) {
|
|
|
141
128
|
}
|
|
142
129
|
```
|
|
143
130
|
|
|
144
|
-
Notice that there is no manual `Set-Cookie` plumbing in the action — the session middleware handles
|
|
145
|
-
that, and the handler returns an ordinary `Response`. Per-browser state enforced this way is still
|
|
146
|
-
bypassable by clearing cookies; if the guarantee needs to survive that, you also need an account
|
|
147
|
-
(see auth providers below).
|
|
131
|
+
Notice that there is no manual `Set-Cookie` plumbing in the action — the session middleware handles that, and the handler returns an ordinary `Response`. Per-browser state enforced this way is still bypassable by clearing cookies; if the guarantee needs to survive that, you also need an account (see auth providers below).
|
|
148
132
|
|
|
149
133
|
## Auth Middleware
|
|
150
134
|
|
|
@@ -296,9 +280,7 @@ let atmosphereProvider = createAtmosphereAuthProvider({
|
|
|
296
280
|
})
|
|
297
281
|
```
|
|
298
282
|
|
|
299
|
-
For Atmosphere-compatible atproto OAuth, create the provider once, call
|
|
300
|
-
`atmosphereProvider.prepare(handleOrDid)` before `startExternalAuth(...)`, then pass the same
|
|
301
|
-
module-scope provider to `finishExternalAuth(...)` and `refreshExternalAuth(...)`.
|
|
283
|
+
For Atmosphere-compatible atproto OAuth, create the provider once, call `atmosphereProvider.prepare(handleOrDid)` before `startExternalAuth(...)`, then pass the same module-scope provider to `finishExternalAuth(...)` and `refreshExternalAuth(...)`.
|
|
302
284
|
|
|
303
285
|
### OAuth controller
|
|
304
286
|
|
|
@@ -336,10 +318,7 @@ export default createController(routes.auth.google, {
|
|
|
336
318
|
|
|
337
319
|
### Refresh stored provider tokens
|
|
338
320
|
|
|
339
|
-
Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC tokens and needs a
|
|
340
|
-
fresh access token from a refresh token. Built-in OIDC providers, X, and Atmosphere support
|
|
341
|
-
refresh-token exchange. If the provider does not rotate the refresh token, the refreshed bundle
|
|
342
|
-
preserves the current one.
|
|
321
|
+
Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC tokens and needs a fresh access token from a refresh token. Built-in OIDC providers, X, and Atmosphere support refresh-token exchange. If the provider does not rotate the refresh token, the refreshed bundle preserves the current one.
|
|
343
322
|
|
|
344
323
|
```typescript
|
|
345
324
|
async function refreshGoogleTokens({ get }) {
|
|
@@ -2,25 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How a Remix Component is shaped and how its state, lifecycle, and updates behave. Read this when
|
|
6
|
-
the task involves:
|
|
5
|
+
How a Remix Component is shaped and how its state, lifecycle, and updates behave. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Writing a component (`handle` plus render function)
|
|
9
8
|
- 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`
|
|
9
|
+
- Using `handle.props`, `handle.update()`, `handle.queueTask()`, `handle.signal`, `handle.id`, or `handle.context`
|
|
12
10
|
- Listening to global events with cleanup tied to the component lifecycle
|
|
13
11
|
|
|
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`.
|
|
12
|
+
For host-element behavior (event handlers, styles, refs, animations), see `mixins-styling-events.md`. For browser hydration, frames, and navigation, see `hydration-frames-navigation.md`.
|
|
17
13
|
|
|
18
14
|
## Phases
|
|
19
15
|
|
|
20
16
|
A component has two phases:
|
|
21
17
|
|
|
22
18
|
1. **Setup phase** — runs once when the component is created
|
|
23
|
-
2. **Render phase** — returned function runs on initial render and every update
|
|
19
|
+
2. **Render phase** — returned zero-argument function runs on initial render and every update
|
|
20
|
+
|
|
21
|
+
The component shape is `function Component(handle: Handle<Props>) { return () => ... }`. Props are available as `handle.props` in setup scope and are updated before every render.
|
|
24
22
|
|
|
25
23
|
```tsx
|
|
26
24
|
import { on, type Handle } from 'remix/ui'
|
|
@@ -43,9 +41,7 @@ function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
|
|
|
43
41
|
|
|
44
42
|
## Props
|
|
45
43
|
|
|
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`:
|
|
44
|
+
Components receive all JSX props through `handle.props`. The object identity is stable for the component lifetime, and its values are updated before each render. Put initialization inputs on normal JSX props and read them from `handle.props`:
|
|
49
45
|
|
|
50
46
|
```tsx
|
|
51
47
|
function Timer(handle: Handle<{ initialSeconds: number; paused?: boolean }>) {
|
|
@@ -57,9 +53,7 @@ function Timer(handle: Handle<{ initialSeconds: number; paused?: boolean }>) {
|
|
|
57
53
|
// Usage: <Timer initialSeconds={60} paused={false} />
|
|
58
54
|
```
|
|
59
55
|
|
|
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.
|
|
56
|
+
Because `handle.props` is stable, destructuring `let { props } = handle` is safe when helpers need to read current values later. Destructuring individual prop values is only a snapshot; prefer `handle.props.name` inside callbacks and render output when values can change.
|
|
63
57
|
|
|
64
58
|
## State Rules
|
|
65
59
|
|
|
@@ -84,8 +78,7 @@ function TodoList(handle: Handle) {
|
|
|
84
78
|
|
|
85
79
|
### `handle.update()`
|
|
86
80
|
|
|
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:
|
|
81
|
+
Schedules a rerender. Returns a promise that resolves with an `AbortSignal` after the update completes. Await it when you need the updated DOM before follow-up work:
|
|
89
82
|
|
|
90
83
|
```tsx
|
|
91
84
|
on('click', async () => {
|
|
@@ -98,9 +91,7 @@ on('click', async () => {
|
|
|
98
91
|
|
|
99
92
|
### `handle.queueTask(task)`
|
|
100
93
|
|
|
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:
|
|
94
|
+
Schedules a task to run after the next update. The task receives an `AbortSignal` that aborts when the component re-renders or is removed. Use for post-render DOM work, reactive data loading, or hydration-sensitive setup:
|
|
104
95
|
|
|
105
96
|
```tsx
|
|
106
97
|
let data = null
|
|
@@ -135,8 +126,7 @@ return () => {
|
|
|
135
126
|
}
|
|
136
127
|
```
|
|
137
128
|
|
|
138
|
-
Avoid creating intermediate state just to trigger `queueTask`. Do the work directly in the handler
|
|
139
|
-
or the queued task.
|
|
129
|
+
Avoid creating intermediate state just to trigger `queueTask`. Do the work directly in the handler or the queued task.
|
|
140
130
|
|
|
141
131
|
### `handle.signal`
|
|
142
132
|
|
|
@@ -188,8 +178,7 @@ Context for ancestor/descendant communication. See the context section below.
|
|
|
188
178
|
|
|
189
179
|
## Context
|
|
190
180
|
|
|
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.
|
|
181
|
+
Use `handle.context.set()` to provide values and `handle.context.get(Provider)` to consume them. `set()` does **not** trigger updates — call `handle.update()` if the tree needs to rerender.
|
|
193
182
|
|
|
194
183
|
```tsx
|
|
195
184
|
function ThemeProvider(handle: Handle<{ children?: RemixNode }, { theme: 'light' | 'dark' }>) {
|
|
@@ -261,8 +250,7 @@ function ThemedContent(handle: Handle) {
|
|
|
261
250
|
|
|
262
251
|
## Global Events
|
|
263
252
|
|
|
264
|
-
Use `addEventListeners(target, handle.signal, listeners)` to listen to global targets with
|
|
265
|
-
automatic cleanup when the component disconnects:
|
|
253
|
+
Use `addEventListeners(target, handle.signal, listeners)` to listen to global targets with automatic cleanup when the component disconnects:
|
|
266
254
|
|
|
267
255
|
```tsx
|
|
268
256
|
import { addEventListeners, type Handle } from 'remix/ui'
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How to author your own reusable host-element behavior with `createMixin`. Read this when the task
|
|
6
|
-
involves:
|
|
5
|
+
How to author your own reusable host-element behavior with `createMixin`. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Combining multiple low-level events or DOM hooks into one semantic mixin
|
|
9
8
|
- Dispatching custom DOM events from a host node
|
|
@@ -14,9 +13,7 @@ For the built-in mixins most code should use, see `mixins-styling-events.md`.
|
|
|
14
13
|
|
|
15
14
|
Use `createMixin` from `remix/ui` to author reusable host-element behavior.
|
|
16
15
|
|
|
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.
|
|
16
|
+
Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`, `attrs`) and animation mixins from `remix/ui/animation`. Create custom mixins when combining multiple low-level events into one semantic event, or when the pattern is reused across components.
|
|
20
17
|
|
|
21
18
|
## Core Semantics
|
|
22
19
|
|
|
@@ -24,8 +21,7 @@ into one semantic event, or when the pattern is reused across components.
|
|
|
24
21
|
2. `insert` is the host-node availability point for imperative setup.
|
|
25
22
|
3. `remove` is teardown for that same lifecycle.
|
|
26
23
|
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.
|
|
24
|
+
5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued work.
|
|
29
25
|
|
|
30
26
|
```tsx
|
|
31
27
|
import { createMixin } from 'remix/ui'
|
|
@@ -71,8 +67,7 @@ let withFocus = createMixin<HTMLElement>((handle) => {
|
|
|
71
67
|
|
|
72
68
|
## Custom Event Mixins
|
|
73
69
|
|
|
74
|
-
Create event mixins when you combine multiple low-level events into one semantic custom event that
|
|
75
|
-
is reused across components.
|
|
70
|
+
Create event mixins when you combine multiple low-level events into one semantic custom event that is reused across components.
|
|
76
71
|
|
|
77
72
|
1. Namespace custom event names (`myapp:*`) to avoid collisions.
|
|
78
73
|
2. Extend `Event` with the data consumers need.
|
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How input becomes a value the app trusts, and how that value reaches storage. Read this when the
|
|
6
|
-
task involves:
|
|
5
|
+
How input becomes a value the app trusts, and how that value reaches storage. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Defining database tables, columns, relations, and migrations
|
|
9
8
|
- Querying or mutating persisted data with `Database`
|
|
10
9
|
- Parsing and validating user input from forms, query strings, or external payloads
|
|
11
10
|
- Choosing between schema-level checks, table validation hooks, and migration-level constraints
|
|
12
11
|
|
|
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`.
|
|
12
|
+
For where validation runs in the request lifecycle, see `routing-and-controllers.md`. For session or identity-bound writes, see `auth-and-sessions.md`.
|
|
15
13
|
|
|
16
14
|
## Table Definitions (`remix/data-table`)
|
|
17
15
|
|
|
@@ -65,22 +63,16 @@ export type OrderWithItems = TableRowWith<typeof orders, 'items'>
|
|
|
65
63
|
| `c.uuid()` | UUID / TEXT |
|
|
66
64
|
| `c.varchar(length)` | VARCHAR |
|
|
67
65
|
|
|
68
|
-
Column modifiers: `.primaryKey()`, `.autoIncrement()`, `.notNull()`, `.unique()`,
|
|
69
|
-
`.references(table, column, fkName?)`, `.onDelete(action)`, `.default(value)`.
|
|
66
|
+
Column modifiers: `.primaryKey()`, `.autoIncrement()`, `.notNull()`, `.unique()`, `.references(table, column, fkName?)`, `.onDelete(action)`, `.default(value)`.
|
|
70
67
|
|
|
71
68
|
Composite primary keys go on the table option, not the column: `primaryKey: ['order_id', 'book_id']`.
|
|
72
69
|
|
|
73
70
|
### Schema vs migrations
|
|
74
71
|
|
|
75
|
-
Column modifiers describe
|
|
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:
|
|
72
|
+
Column modifiers on runtime `table(...)` definitions in `app/data/schema.ts` describe app-facing column metadata. They do not create or update database tables by themselves. The source of truth for actual DDL and constraints is your hand-written SQL migration files. Two valid patterns:
|
|
79
73
|
|
|
80
|
-
- **
|
|
81
|
-
|
|
82
|
-
- **Bare columns in schema, full modifiers in migrations** — schema describes what the app reads
|
|
83
|
-
and writes; migrations own the DDL and constraints.
|
|
74
|
+
- **Mirror constraints in schema and SQL** — table definitions stay useful as schema-level docs, and migrations still own the actual DDL.
|
|
75
|
+
- **Bare columns in schema, constraints in SQL** — schema describes what the app reads and writes; migrations own the DDL and constraints.
|
|
84
76
|
|
|
85
77
|
Pick one and apply it consistently across the app.
|
|
86
78
|
|
|
@@ -88,8 +80,7 @@ Pick one and apply it consistently across the app.
|
|
|
88
80
|
|
|
89
81
|
Tables can define validation and lifecycle hooks:
|
|
90
82
|
|
|
91
|
-
- `validate` runs before `create` and `update` writes and should return either `{ value }` or
|
|
92
|
-
`{ issues }`
|
|
83
|
+
- `validate` runs before `create` and `update` writes and should return either `{ value }` or `{ issues }`
|
|
93
84
|
- `beforeWrite` can normalize or veto `create`/`update` values
|
|
94
85
|
- `afterWrite` observes completed `create`/`update` operations
|
|
95
86
|
- `beforeDelete` and `afterDelete` observe or veto deletes
|
|
@@ -135,9 +126,7 @@ let adapter = createSqliteDatabaseAdapter(sqlite)
|
|
|
135
126
|
export let db = createDatabase(adapter)
|
|
136
127
|
```
|
|
137
128
|
|
|
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.
|
|
129
|
+
`createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
|
|
141
130
|
|
|
142
131
|
### Database middleware
|
|
143
132
|
|
|
@@ -195,46 +184,51 @@ let featured = await db.findMany(books, {
|
|
|
195
184
|
|
|
196
185
|
## Migrations
|
|
197
186
|
|
|
187
|
+
Migrations are plain SQL files. Each migration is a directory named `YYYYMMDDHHmmss_<slug>/` containing a hand-written `up.sql` (required) and an optional `down.sql` (omit for irreversible migrations).
|
|
188
|
+
|
|
189
|
+
```txt
|
|
190
|
+
db/
|
|
191
|
+
migrations/
|
|
192
|
+
20260228090000_create_users/
|
|
193
|
+
up.sql
|
|
194
|
+
down.sql
|
|
195
|
+
20260301083000_add_books_search_index/
|
|
196
|
+
up.sql
|
|
197
|
+
```
|
|
198
|
+
|
|
198
199
|
### Writing migrations
|
|
199
200
|
|
|
200
|
-
|
|
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
|
-
},
|
|
201
|
+
Write standard SQL in `up.sql` and `down.sql`:
|
|
217
202
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
203
|
+
```sql
|
|
204
|
+
-- up.sql
|
|
205
|
+
create table users (
|
|
206
|
+
id integer primary key autoincrement,
|
|
207
|
+
email text not null unique,
|
|
208
|
+
name text not null
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
create index users_email_idx on users (email);
|
|
222
212
|
```
|
|
223
213
|
|
|
224
|
-
|
|
214
|
+
```sql
|
|
215
|
+
-- down.sql
|
|
216
|
+
drop table if exists users;
|
|
217
|
+
```
|
|
225
218
|
|
|
226
|
-
|
|
227
|
-
import { createMigration } from 'remix/data-table/migrations'
|
|
228
|
-
import { users, authAccounts } from '../../app/data/schema.ts'
|
|
219
|
+
Do **not** import app code (e.g. `app/data/schema.ts`) into migration files. Migrations must be stable, immutable artifacts — importing live schema definitions creates drift between what the migration meant when it was written and what it does when replayed later. SQL files guarantee stability because they cannot import anything.
|
|
229
220
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
221
|
+
### Transaction modes
|
|
222
|
+
|
|
223
|
+
Migrations run inside a transaction by default (when the adapter supports transactional DDL). Override per migration with a directive comment in `up.sql`:
|
|
224
|
+
|
|
225
|
+
```sql
|
|
226
|
+
-- data-table/transaction: none
|
|
227
|
+
create index concurrently users_email_idx on users (email);
|
|
236
228
|
```
|
|
237
229
|
|
|
230
|
+
Modes: `auto` (default — wrap when supported), `required` (wrap; throw if unsupported), `none` (never wrap).
|
|
231
|
+
|
|
238
232
|
### Running migrations
|
|
239
233
|
|
|
240
234
|
```typescript
|
|
@@ -246,15 +240,11 @@ let runner = createMigrationRunner(adapter, migrations)
|
|
|
246
240
|
await runner.up()
|
|
247
241
|
```
|
|
248
242
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Name migration files with a timestamp prefix: `20260228090000_create_users.ts`. Place them in
|
|
252
|
-
`db/migrations/`.
|
|
243
|
+
The runner checksums each `up.sql` and detects drift if a previously applied migration changes. Use `runner.status()` to inspect applied/pending/drifted state, and `runner.down()` to revert.
|
|
253
244
|
|
|
254
245
|
## Input Validation (`remix/data-schema`)
|
|
255
246
|
|
|
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.
|
|
247
|
+
Use `data-schema` to validate user input (forms, query params, API payloads). This is separate from table-level `validate` hooks which run at persistence.
|
|
258
248
|
|
|
259
249
|
### Schema builders
|
|
260
250
|
|
|
@@ -295,9 +285,7 @@ let { name, email, password } = s.parse(signupSchema, formData)
|
|
|
295
285
|
|
|
296
286
|
There are two ways to get a `FormData` value inside an action.
|
|
297
287
|
|
|
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.
|
|
288
|
+
The recommended way: register `formData()` middleware in the root stack and read with `get(FormData)`. The body is parsed once per request, and the typed `FormData` value flows through the context system. This also lets `methodOverride()` and CSRF middleware work uniformly.
|
|
301
289
|
|
|
302
290
|
```typescript
|
|
303
291
|
import { formData } from 'remix/middleware/form-data'
|
|
@@ -310,15 +298,11 @@ let router = createRouter({
|
|
|
310
298
|
let parsed = s.parseSafe(signupSchema, get(FormData))
|
|
311
299
|
```
|
|
312
300
|
|
|
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.
|
|
301
|
+
The fallback: `await request.formData()` directly. This works without middleware and is fine for small one-off cases, but it bypasses the context system, runs once per call site, and doesn't compose with middleware that depends on parsed form fields.
|
|
316
302
|
|
|
317
303
|
### Safe parsing
|
|
318
304
|
|
|
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:
|
|
305
|
+
`s.parse` throws on invalid input. `s.parseSafe` returns a tagged result and is usually what an action wants, since validation failure is an expected outcome (re-render the form with errors) rather than an exception:
|
|
322
306
|
|
|
323
307
|
```typescript
|
|
324
308
|
let result = s.parseSafe(signupSchema, get(FormData))
|
|
@@ -328,13 +312,11 @@ if (!result.success) {
|
|
|
328
312
|
let { name, email, password } = result.value
|
|
329
313
|
```
|
|
330
314
|
|
|
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.
|
|
315
|
+
Returning a `Response` for validation failures keeps the route contract honest: the same action returns 200 on success, 400 with errors on bad input, no out-of-band exception flow.
|
|
333
316
|
|
|
334
317
|
### Transforming validated output
|
|
335
318
|
|
|
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(...)`:
|
|
319
|
+
Use `.transform(...)` when a schema should validate one shape but return another value or output type. Transforms run after validation and compose with `.pipe(...)` and `.refine(...)`:
|
|
338
320
|
|
|
339
321
|
```typescript
|
|
340
322
|
import * as coerce from 'remix/data-schema/coerce'
|
|
@@ -356,14 +338,9 @@ let { page, q } = s.parse(pageSchema, formData)
|
|
|
356
338
|
|
|
357
339
|
Avoid these shapes when reading and validating input:
|
|
358
340
|
|
|
359
|
-
- **Raw `formData.get('name')` plus an `if (typeof name !== 'string')` guard**, then a thrown
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
341
|
+
- **Raw `formData.get('name')` plus an `if (typeof name !== 'string')` guard**, then a thrown custom error. This reinvents what `data-schema` already does, loses the typed result, and pushes error translation into a `try/catch` instead of a return value.
|
|
342
|
+
- **Letting route-local domain errors leak out of the action.** Translate expected outcomes (bad input, missing record, duplicate entry) into the `Response` the route means to return instead of throwing a custom `Error` subclass with a `status` field and catching it later.
|
|
343
|
+
- **Trusting `params`, query strings, or external payloads without a schema.** Anything that crosses a trust boundary should be parsed before it reaches business logic.
|
|
367
344
|
|
|
368
345
|
### Common patterns
|
|
369
346
|
|