@remix-run/cli 0.3.4 → 0.4.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.
- package/README.md +105 -1
- package/dist/index.js +1 -1
- package/dist/lib/app-root.d.ts +11 -0
- package/dist/lib/app-root.d.ts.map +1 -0
- package/dist/lib/app-root.js +38 -0
- package/dist/lib/bootstrap-project.js +2 -2
- package/dist/lib/cli-context.d.ts +6 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +12 -1
- package/dist/lib/cli.d.ts +3 -2
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +42 -14
- package/dist/lib/commands/completion.js +4 -4
- package/dist/lib/commands/db.d.ts +4 -0
- package/dist/lib/commands/db.d.ts.map +1 -0
- package/dist/lib/commands/db.js +283 -0
- package/dist/lib/commands/doctor.d.ts +5 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -1
- package/dist/lib/commands/doctor.js +26 -251
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +17 -9
- package/dist/lib/commands/new.js +6 -6
- package/dist/lib/commands/routes.js +6 -6
- package/dist/lib/commands/test.d.ts +20 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +353 -10
- package/dist/lib/commands/version.js +3 -3
- package/dist/lib/completion.d.ts +1 -1
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +229 -6
- package/dist/lib/controller-files.d.ts +0 -8
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +0 -23
- package/dist/lib/controller-ownership.d.ts +4 -12
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +31 -127
- package/dist/lib/database-command.d.ts +20 -0
- package/dist/lib/database-command.d.ts.map +1 -0
- package/dist/lib/database-command.js +7 -0
- package/dist/lib/doctor/controller-findings.d.ts +2 -2
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +23 -39
- package/dist/lib/doctor/controllers.d.ts +1 -2
- package/dist/lib/doctor/controllers.d.ts.map +1 -1
- package/dist/lib/doctor/controllers.js +4 -7
- package/dist/lib/doctor/environment.js +2 -2
- package/dist/lib/doctor/fixes.js +1 -1
- package/dist/lib/doctor/project.d.ts.map +1 -1
- package/dist/lib/doctor/project.js +3 -212
- package/dist/lib/doctor/run.d.ts +9 -0
- package/dist/lib/doctor/run.d.ts.map +1 -0
- package/dist/lib/doctor/run.js +237 -0
- package/dist/lib/doctor/types.d.ts +2 -1
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/doctor/types.js +4 -1
- package/dist/lib/errors.d.ts +30 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +55 -0
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/parse-args.d.ts.map +1 -1
- package/dist/lib/parse-args.js +17 -4
- package/dist/lib/remix-config.d.ts +75 -0
- package/dist/lib/remix-config.d.ts.map +1 -0
- package/dist/lib/remix-config.js +411 -0
- package/dist/lib/reporter.js +1 -1
- package/dist/lib/route-map.js +13 -33
- package/package.json +13 -7
- package/schema/remix.json +242 -0
- package/src/lib/app-root.ts +46 -0
- package/src/lib/cli-context.ts +17 -0
- package/src/lib/cli.ts +41 -6
- package/src/lib/commands/db.ts +377 -0
- package/src/lib/commands/doctor.ts +27 -329
- package/src/lib/commands/help.ts +10 -1
- package/src/lib/commands/test.ts +453 -12
- package/src/lib/completion.ts +321 -6
- package/src/lib/controller-files.ts +0 -38
- package/src/lib/controller-ownership.ts +50 -203
- package/src/lib/database-command.ts +31 -0
- package/src/lib/doctor/controller-findings.ts +30 -47
- package/src/lib/doctor/controllers.ts +2 -6
- package/src/lib/doctor/project.ts +0 -236
- package/src/lib/doctor/run.ts +311 -0
- package/src/lib/doctor/types.ts +6 -5
- package/src/lib/errors.ts +65 -0
- package/src/lib/parse-args.ts +19 -3
- package/src/lib/remix-config.ts +639 -0
- package/src/lib/route-map.ts +11 -48
- package/template/.agents/skills/remix/SKILL.md +38 -27
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
- package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
- package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
- package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
- package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
- package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
- package/template/AGENTS.md +6 -3
- package/template/README.md +6 -3
- package/template/app/actions/controller.tsx +1 -1
- package/template/app/{ui → actions}/document.tsx +7 -5
- package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
- package/template/app/actions/public/entry.ts +48 -0
- package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
- package/template/app/assets.ts +15 -2
- package/template/hmr.ts +46 -0
- package/template/package.json +1 -0
- package/template/server.ts +8 -1
- package/template/tsconfig.json +1 -1
- package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
- package/dist/lib/doctor/controller-fix-plans.js +0 -119
- package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
- package/dist/lib/doctor/controller-placeholders.js +0 -52
- package/src/lib/doctor/controller-fix-plans.ts +0 -168
- package/src/lib/doctor/controller-placeholders.ts +0 -70
- package/template/app/assets/entry.ts +0 -17
package/src/lib/route-map.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises'
|
|
2
1
|
import * as path from 'node:path'
|
|
3
2
|
import * as process from 'node:process'
|
|
4
3
|
import { spawn } from 'node:child_process'
|
|
5
4
|
import { fileURLToPath } from 'node:url'
|
|
6
5
|
|
|
6
|
+
import { findAppRoot } from './app-root.ts'
|
|
7
7
|
import {
|
|
8
8
|
ROOT_ROUTE_NAME,
|
|
9
9
|
inspectControllerOwnership,
|
|
10
10
|
type ControllerOwnership,
|
|
11
11
|
type OwnedSubtree,
|
|
12
|
-
type
|
|
12
|
+
type RouteDirectory,
|
|
13
13
|
} from './controller-ownership.ts'
|
|
14
14
|
import {
|
|
15
15
|
routeMapLoaderFailed,
|
|
@@ -152,29 +152,17 @@ function decorateRouteTree(
|
|
|
152
152
|
ownership.routeDirectories.map((directory) => [directory.routeName, directory]),
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
-
return decorateRouteTreeWithLookup(
|
|
156
|
-
rawTree,
|
|
157
|
-
subtreesByRouteName,
|
|
158
|
-
directoriesByRouteName,
|
|
159
|
-
ownership.scan.routeDirectoryPaths,
|
|
160
|
-
)
|
|
155
|
+
return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName)
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
function decorateRouteTreeWithLookup(
|
|
164
159
|
rawTree: RawRouteTreeNode[],
|
|
165
160
|
subtreesByRouteName: Map<string, OwnedSubtree>,
|
|
166
|
-
directoriesByRouteName: Map<string,
|
|
167
|
-
actualRouteDirectories: Set<string>,
|
|
161
|
+
directoriesByRouteName: Map<string, RouteDirectory>,
|
|
168
162
|
parentSegments: string[] = [],
|
|
169
163
|
): RouteTreeNode[] {
|
|
170
164
|
return rawTree.map((rawNode) => {
|
|
171
|
-
let owner = getRouteOwner(
|
|
172
|
-
rawNode,
|
|
173
|
-
parentSegments,
|
|
174
|
-
subtreesByRouteName,
|
|
175
|
-
directoriesByRouteName,
|
|
176
|
-
actualRouteDirectories,
|
|
177
|
-
)
|
|
165
|
+
let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName)
|
|
178
166
|
let nextParentSegments =
|
|
179
167
|
rawNode.kind === 'group' ? [...parentSegments, rawNode.key] : parentSegments
|
|
180
168
|
|
|
@@ -185,7 +173,6 @@ function decorateRouteTreeWithLookup(
|
|
|
185
173
|
rawNode.children,
|
|
186
174
|
subtreesByRouteName,
|
|
187
175
|
directoriesByRouteName,
|
|
188
|
-
actualRouteDirectories,
|
|
189
176
|
nextParentSegments,
|
|
190
177
|
)
|
|
191
178
|
: [],
|
|
@@ -203,8 +190,7 @@ function getRouteOwner(
|
|
|
203
190
|
rawNode: RawRouteTreeNode,
|
|
204
191
|
parentSegments: string[],
|
|
205
192
|
subtreesByRouteName: Map<string, OwnedSubtree>,
|
|
206
|
-
directoriesByRouteName: Map<string,
|
|
207
|
-
actualRouteDirectories: Set<string>,
|
|
193
|
+
directoriesByRouteName: Map<string, RouteDirectory>,
|
|
208
194
|
): RouteTreeOwner {
|
|
209
195
|
let ownerRouteName =
|
|
210
196
|
rawNode.kind === 'group'
|
|
@@ -230,7 +216,7 @@ function getRouteOwner(
|
|
|
230
216
|
}
|
|
231
217
|
|
|
232
218
|
return {
|
|
233
|
-
exists:
|
|
219
|
+
exists: directory.exists,
|
|
234
220
|
kind: 'directory',
|
|
235
221
|
path: directory.directoryPath,
|
|
236
222
|
}
|
|
@@ -296,36 +282,13 @@ function assertRawRouteTreeNode(value: unknown): RawRouteTreeNode {
|
|
|
296
282
|
}
|
|
297
283
|
|
|
298
284
|
async function findRemixAppRoot(startDir: string): Promise<string> {
|
|
299
|
-
let
|
|
300
|
-
|
|
301
|
-
while (true) {
|
|
302
|
-
if (await pathExists(path.join(currentDir, 'app', 'routes.ts'))) {
|
|
303
|
-
return currentDir
|
|
304
|
-
}
|
|
285
|
+
let appRoot = await findAppRoot(startDir, 'app/routes.ts')
|
|
305
286
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
break
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
currentDir = parentDir
|
|
287
|
+
if (appRoot == null) {
|
|
288
|
+
throw routesFileNotFound(startDir)
|
|
312
289
|
}
|
|
313
290
|
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function pathExists(filePath: string): Promise<boolean> {
|
|
318
|
-
try {
|
|
319
|
-
await fs.access(filePath)
|
|
320
|
-
return true
|
|
321
|
-
} catch (error) {
|
|
322
|
-
let nodeError = error as NodeJS.ErrnoException
|
|
323
|
-
if (nodeError.code === 'ENOENT') {
|
|
324
|
-
return false
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
throw error
|
|
328
|
-
}
|
|
291
|
+
return appRoot
|
|
329
292
|
}
|
|
330
293
|
|
|
331
294
|
function getRouteMapWorkerPath(): string {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: remix
|
|
3
|
-
description: Build and review Remix 3 applications using the `remix` npm package and subpath imports. Use when working on Remix app structure, routes, controllers, middleware, validation, data access, auth, sessions, file uploads, server setup, UI components, hydration, navigation, or tests.
|
|
3
|
+
description: Build and review Remix 3 applications using the `remix` npm package and subpath imports. Use when working on Remix app structure, routes, controllers, middleware, validation, data access, auth, sessions, file uploads, server setup, UI components, hydration, HMR, navigation, or tests.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Build a Remix App
|
|
@@ -42,19 +42,19 @@ Classify the task first, then load the smallest useful reference set. Each refer
|
|
|
42
42
|
|
|
43
43
|
Use the table below to find candidates. Loading more than two or three files at once is usually a sign that the task hasn't been narrowed enough yet.
|
|
44
44
|
|
|
45
|
-
| Task involves...
|
|
46
|
-
|
|
|
47
|
-
| Defining URLs, writing controllers and actions, returning responses
|
|
48
|
-
| Composing the request lifecycle, ordering middleware, bridging to a server
|
|
49
|
-
| Compiling and serving browser modules, asset URL namespaces, preloads
|
|
50
|
-
| Parsing input, validating with schemas, defining tables, querying, migrations
|
|
51
|
-
| Per-browser state, login flows, route protection, identity
|
|
52
|
-
| Component setup, state, lifecycle, updates, `queueTask`, context
|
|
53
|
-
| Event handlers, styles, refs, click/key behavior, simple animations
|
|
54
|
-
| `clientEntry`, `run`, `<Frame>`, navigation, `<head>`
|
|
55
|
-
| Router tests, component tests, test isolation
|
|
56
|
-
| Spring physics, tweens, layout transitions
|
|
57
|
-
| Authoring custom reusable mixins
|
|
45
|
+
| Task involves... | Start with |
|
|
46
|
+
| ------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
47
|
+
| Defining URLs, writing controllers and actions, returning responses | `references/routing-and-controllers.md` |
|
|
48
|
+
| Composing the request lifecycle, ordering middleware, bridging to a server, development HMR | `references/middleware-and-server.md` |
|
|
49
|
+
| Compiling and serving browser modules, asset URL namespaces, preloads, browser HMR | `references/assets-and-browser-modules.md` |
|
|
50
|
+
| Parsing input, validating with schemas, defining tables, querying, migrations | `references/data-and-validation.md` |
|
|
51
|
+
| Per-browser state, login flows, route protection, identity | `references/auth-and-sessions.md` |
|
|
52
|
+
| Component setup, state, lifecycle, updates, `queueTask`, context | `references/component-model.md` |
|
|
53
|
+
| Event handlers, styles, refs, click/key behavior, simple animations | `references/mixins-styling-events.md` |
|
|
54
|
+
| `clientEntry`, `run`, `<Frame>`, navigation, browser HMR update handling, `<head>` | `references/hydration-frames-navigation.md` |
|
|
55
|
+
| Router tests, component tests, test isolation | `references/testing-patterns.md` |
|
|
56
|
+
| Spring physics, tweens, layout transitions | `references/animate-elements.md` |
|
|
57
|
+
| Authoring custom reusable mixins | `references/create-mixins.md` |
|
|
58
58
|
|
|
59
59
|
Common bundles:
|
|
60
60
|
|
|
@@ -62,6 +62,7 @@ Common bundles:
|
|
|
62
62
|
- **Protected area** -> auth and sessions, routing, testing
|
|
63
63
|
- **Interactive widget** -> component model, mixins and styling; add hydration only if it runs in the browser
|
|
64
64
|
- **Browser asset pipeline** -> assets and browser modules, hydration, middleware and server
|
|
65
|
+
- **Development HMR** -> middleware and server, assets and browser modules, hydration
|
|
65
66
|
- **File upload** -> middleware and server, data and validation, testing
|
|
66
67
|
- **Navigation or frames** -> hydration, frames, navigation
|
|
67
68
|
|
|
@@ -83,19 +84,19 @@ Use these root directories consistently:
|
|
|
83
84
|
|
|
84
85
|
- `app/` for runtime application code
|
|
85
86
|
- `db/` for migrations and local database files
|
|
86
|
-
- `public/` for static assets served as-is
|
|
87
|
+
- root `public/` for static assets served as-is from the app root
|
|
87
88
|
- `test/` for shared helpers, fixtures, and integration coverage
|
|
88
89
|
- `tmp/` for uploads, caches, local session files, and other scratch data
|
|
89
90
|
|
|
90
91
|
Inside `app/`, organize by responsibility:
|
|
91
92
|
|
|
92
|
-
- `assets/` for client entrypoints and client-owned browser behavior
|
|
93
93
|
- `actions/` for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areas
|
|
94
94
|
- `data/` for schema, queries, persistence setup, migrations, and runtime data initialization
|
|
95
95
|
- `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database injection
|
|
96
|
+
- `public/` directories inside the narrowest owner for browser-reachable source code, with the browser runtime entrypoint at `app/actions/public/entry.ts`
|
|
96
97
|
- `ui/` for shared cross-route UI primitives
|
|
97
98
|
- `utils/` only for genuinely cross-layer helpers that do not clearly belong elsewhere
|
|
98
|
-
- `routes.ts` for the route contract
|
|
99
|
+
- `routes.ts` for the shared server-and-browser route contract and type-safe href generation
|
|
99
100
|
- `router.ts` for router setup and wiring
|
|
100
101
|
|
|
101
102
|
### Placement Precedence
|
|
@@ -147,10 +148,11 @@ When code could live in multiple places:
|
|
|
147
148
|
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types are part of the route contract
|
|
148
149
|
- Make the server route correct first. A POST should already return the right HTML, redirect, or error response on its own before `clientEntry(...)` layers interactivity on top
|
|
149
150
|
- Validate input at the boundary using `remix/data-schema` (and `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a return value instead of an exception
|
|
150
|
-
- Derive `AppContext` from the middleware stack so `get(
|
|
151
|
+
- Derive `AppContext` from the middleware stack so `get(databaseContext)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
|
|
151
152
|
- Outside actions and controllers, only use `getContext()` when `asyncContext()` is in the middleware stack
|
|
152
153
|
- Remix Component is not React: write `function Name(handle: Handle<Props>) { return () => ... }`, read props from `handle.props`, keep state in setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive work in event handlers or `queueTask(...)`, not in render
|
|
153
154
|
- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling instead of inventing custom host prop conventions. Use `mix={[...]}` only when composing multiple mixins
|
|
155
|
+
- Keep short, one-off static styles inline with `mix={css(...)}`. Extract a module-scoped style descriptor when it forms a reused visual recipe, has substantial selectors, media queries, or keyframes, or is large enough to obscure the component. Export a style descriptor only after multiple modules need the same visual recipe; otherwise keep it with its narrowest owner
|
|
154
156
|
- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions, class instances, or opaque runtime objects
|
|
155
157
|
|
|
156
158
|
## Security And Session Defaults
|
|
@@ -205,7 +207,11 @@ Use this map to find the right package quickly. Each entry says what the package
|
|
|
205
207
|
- `remix/router` — the router itself. Use for `createRouter`, controllers, middleware types, and registering routes
|
|
206
208
|
- `remix/routes` — declarative route builders. Use for `route`, `get`, `post`, `put`, `del`, `form`, `resources` when defining `app/routes.ts`
|
|
207
209
|
- `remix/node-fetch-server` — default Node adapter for new apps. Use `createRequestListener` with `node:http`, `node:https`, or `node:http2` in `server.ts` when booting the template-style app
|
|
208
|
-
- `remix/
|
|
210
|
+
- `remix/node-hmr` — optional development Node HMR runner for rapid UI edits. Use `run` in `hmr.ts` to supervise `server.ts` behind an `hmr` script, and use `createHmrReadyFetch` when a stable public proxy should wait for child server readiness during updates
|
|
211
|
+
- `remix/node-hmr/runtime` — child-process runtime API for code running under `remix/node-hmr`. Use to create browser HMR channels for asset servers and to emit server readiness after the child server starts listening
|
|
212
|
+
- `remix/node-hmr/types` — type-only entry for `import.meta.hot` in Node modules
|
|
213
|
+
- `remix/assets` — browser asset server. Use for `createAssetServer` when serving compiled scripts and styles, getting public hrefs, emitting preloads, and wiring browser HMR. Configure a `basePath`, keep `fileMap` URL patterns relative to it, use `allowFiles`/`denyFiles` for path and glob rules, and use exact package names in `allowPackages` for package-level access. Shared compiler options such as `target`, `sourceMaps`, `sourceMapSourcePaths`, and `minify` live at the top level
|
|
214
|
+
- `remix/assets/types/hmr` — type-only entry for `import.meta.hot` in browser modules compiled by `remix/assets`
|
|
209
215
|
- `remix/headers` — `SuperHeaders` plus typed header parsers and builders. Use the default export when you want a `Headers` subclass with typed accessors like `headers.contentType`, `headers.cacheControl`, and `headers.setCookie`; use named classes such as `CacheControl`, `ContentDisposition`, and `Vary` when working with individual header values
|
|
210
216
|
- `remix/response/redirect` — `redirect(href, status?)`. Use for the canonical "POST then redirect" pattern and other location changes
|
|
211
217
|
- `remix/response/html` — `createHtmlResponse`. Use when you need an HTML `Response` from a string or stream without rendering through `remix/ui`
|
|
@@ -222,12 +228,12 @@ Use this map to find the right package quickly. Each entry says what the package
|
|
|
222
228
|
- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed value
|
|
223
229
|
- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use in actions that read browser forms
|
|
224
230
|
- `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared later
|
|
225
|
-
- `remix/data-table` — typed tables and
|
|
226
|
-
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` —
|
|
227
|
-
- `remix/data-table/migrations` — migration authoring and
|
|
231
|
+
- `remix/data-table` — typed tables and the shared `Database` API. Use `table` and `column` when modeling persisted data, then create a concrete database from the matching dialect package. Integration packages can implement `DatabaseDriver` and extend `Database` to add another SQL dialect
|
|
232
|
+
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — concrete database integrations. Use `createSqliteDatabase`, `createPostgresDatabase`, or `createMysqlDatabase`. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
|
|
233
|
+
- `remix/data-table/migrations` — migration authoring and registries. Use for `createMigration` and `createMigrationRegistry`; run migrations with `Database.migrate()`
|
|
228
234
|
- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in startup scripts that apply migrations
|
|
229
235
|
- `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses need set or comparison logic
|
|
230
|
-
- `remix/data-table/sql-helpers` — SQL helper utilities for
|
|
236
|
+
- `remix/data-table/sql-helpers` — SQL helper utilities for database integrations or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
|
|
231
237
|
|
|
232
238
|
### Auth, Sessions, and Cookies
|
|
233
239
|
|
|
@@ -244,6 +250,10 @@ Use this map to find the right package quickly. Each entry says what the package
|
|
|
244
250
|
|
|
245
251
|
- `remix/ui` — the component runtime: components, core mixins, `clientEntry`, `run`, `<Frame>`, navigation helpers, and `createRoot`. Use for app UI behavior
|
|
246
252
|
- `remix/ui/server` — server rendering: `renderToStream`, `renderToString`. Use in the `app/actions/render.tsx` helper that returns HTML responses
|
|
253
|
+
- `remix/ui-hmr` — direct Remix UI component HMR transforms. Use only when writing a custom module hook or build integration
|
|
254
|
+
- `remix/ui-hmr/node` — Node import hook for Remix UI component HMR. Use with `--import remix/ui-hmr/node` in development servers that run through `remix/node-hmr`
|
|
255
|
+
- `remix/ui-hmr/assets` — `remix/assets` loader for Remix UI component HMR. Use `uiHmr()` in `createAssetServer({ scripts: { loaders } })` during development
|
|
256
|
+
- `remix/ui/dev/refresh` — development refresh support used by HMR tooling, not normal application code
|
|
247
257
|
- `remix/ui/animation` — animation APIs: `animateEntrance`, `animateExit`, `animateLayout`, `spring`, `tween`, and `easings`
|
|
248
258
|
- `remix/ui/<primitive>` — UI primitives, mixins, and component helpers. Current subpaths include `remix/ui/accordion`, `remix/ui/anchor`, `remix/ui/button`, `remix/ui/checkbox`, `remix/ui/combobox`, `remix/ui/input`, `remix/ui/listbox`, `remix/ui/menu`, `remix/ui/popover`, and `remix/ui/select`
|
|
249
259
|
- `remix/ui/test` — component test rendering helpers such as `render`
|
|
@@ -309,17 +319,18 @@ export const routes = route({
|
|
|
309
319
|
```typescript
|
|
310
320
|
import { createController } from 'remix/router'
|
|
311
321
|
|
|
322
|
+
import { databaseContext } from '../middleware/database.ts'
|
|
312
323
|
import { routes } from '../routes.ts'
|
|
313
324
|
|
|
314
325
|
export default createController(routes.books, {
|
|
315
326
|
actions: {
|
|
316
327
|
async index({ get }) {
|
|
317
|
-
let db = get(
|
|
328
|
+
let db = get(databaseContext)
|
|
318
329
|
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
|
|
319
330
|
return render(<BooksIndexPage allBooks={allBooks} />)
|
|
320
331
|
},
|
|
321
332
|
async show({ get, params }) {
|
|
322
|
-
let db = get(
|
|
333
|
+
let db = get(databaseContext)
|
|
323
334
|
let book = await db.findOne(books, { where: { slug: params.slug } })
|
|
324
335
|
if (!book) return new Response('Not Found', { status: 404 })
|
|
325
336
|
return render(<BookShowPage book={book} />)
|
|
@@ -384,8 +395,8 @@ import { redirect } from 'remix/response/redirect'
|
|
|
384
395
|
import * as s from 'remix/data-schema'
|
|
385
396
|
import * as f from 'remix/data-schema/form-data'
|
|
386
397
|
import { Session } from 'remix/session'
|
|
387
|
-
import { Database } from 'remix/data-table'
|
|
388
398
|
|
|
399
|
+
import { databaseContext } from '../middleware/database.ts'
|
|
389
400
|
import { routes } from '../routes.ts'
|
|
390
401
|
|
|
391
402
|
let bookSchema = f.object({
|
|
@@ -401,7 +412,7 @@ export default createController(routes.books, {
|
|
|
401
412
|
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
|
|
402
413
|
}
|
|
403
414
|
|
|
404
|
-
let db = get(
|
|
415
|
+
let db = get(databaseContext)
|
|
405
416
|
let book = await db.create(books, parsed.value)
|
|
406
417
|
|
|
407
418
|
let session = get(Session)
|
|
@@ -4,16 +4,17 @@
|
|
|
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`, `
|
|
7
|
+
- Configuring `createAssetServer` (`basePath`, `fileMap`, `allowFiles`, `allowPackages`, `denyFiles`, fingerprinting, compiler options)
|
|
8
8
|
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs
|
|
9
9
|
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
|
|
10
|
-
-
|
|
10
|
+
- Enabling browser HMR for source-served modules
|
|
11
|
+
- Keeping files such as tests out of the browser via `denyFiles` rules
|
|
11
12
|
|
|
12
|
-
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration, see `hydration-frames-navigation.md`.
|
|
13
|
+
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration and browser update handling, see `hydration-frames-navigation.md`. For the Node HMR runner and browser HMR channel, see `middleware-and-server.md`.
|
|
13
14
|
|
|
14
15
|
## When To Reach For It
|
|
15
16
|
|
|
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
|
|
17
|
+
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, and monorepo code that should be compiled and served under a public URL namespace.
|
|
17
18
|
|
|
18
19
|
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.
|
|
19
20
|
|
|
@@ -35,8 +36,9 @@ let assetServer = createAssetServer({
|
|
|
35
36
|
'app/*path': 'app/*path',
|
|
36
37
|
'node_modules/*path': 'node_modules/*path',
|
|
37
38
|
},
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
40
|
+
allowPackages: ['remix'],
|
|
41
|
+
denyFiles: ['app/**/*.test.*'],
|
|
40
42
|
target: { es: '2020', chrome: '109', safari: '16.4' },
|
|
41
43
|
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
|
|
42
44
|
minify: process.env.NODE_ENV === 'production',
|
|
@@ -58,8 +60,13 @@ export default createController(routes, {
|
|
|
58
60
|
|
|
59
61
|
## Rules
|
|
60
62
|
|
|
61
|
-
- Treat `
|
|
62
|
-
-
|
|
63
|
+
- Treat `allowFiles`/`allowPackages` and `denyFiles` as the security boundary for browser-reachable source files.
|
|
64
|
+
- Put browser-reachable app source in a `public/` directory inside `app/`, beside its narrowest owner, such as `app/ui/public/` or `app/actions/cart/public/`.
|
|
65
|
+
- Every local dependency in a browser module graph must match `allowFiles`, so keep the whole graph inside those `public/` directories. `app/routes.ts` is allowed separately so browser modules can build type-safe links with `routes.*.href(...)`.
|
|
66
|
+
- Deny test modules with `denyFiles` so tests can be colocated inside a `public/` directory without becoming browser-reachable.
|
|
67
|
+
- Use `allowFiles` and `denyFiles` for file paths and globs. Relative values resolve from `rootDir`.
|
|
68
|
+
- Use `allowPackages` for exact package names, not globs or subpaths. Packages allowed by `allowPackages` also allow their installed `dependencies` and `optionalDependencies`; peer dependencies must be listed explicitly if they should be browser-reachable.
|
|
69
|
+
- `denyFiles` takes precedence over both file and package allow rules.
|
|
63
70
|
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
|
|
64
71
|
- `basePath` is the public URL namespace handled by the asset server.
|
|
65
72
|
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides.
|
|
@@ -71,8 +78,8 @@ export default createController(routes, {
|
|
|
71
78
|
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.
|
|
72
79
|
|
|
73
80
|
```typescript
|
|
74
|
-
let entryHref = await assetServer.getHref('app/
|
|
75
|
-
let
|
|
81
|
+
let entryHref = await assetServer.getHref('app/actions/public/entry.ts')
|
|
82
|
+
let entryPreloads = await assetServer.getPreloads('app/actions/public/entry.ts')
|
|
76
83
|
```
|
|
77
84
|
|
|
78
85
|
Use this when rendering documents or layouts that boot browser behavior with a known client entry.
|
|
@@ -86,6 +93,8 @@ In development:
|
|
|
86
93
|
- Keep `watch` enabled so source changes are picked up without restarting the server
|
|
87
94
|
- Prefer stable URLs with normal revalidation
|
|
88
95
|
- Enable source maps when debugging browser code
|
|
96
|
+
- Use `hmr` only when the app is running under `remix/node-hmr`
|
|
97
|
+
- Use `scripts.loaders` for development-only browser transforms such as `uiHmr()`
|
|
89
98
|
|
|
90
99
|
In deployment:
|
|
91
100
|
|
|
@@ -95,6 +104,39 @@ In deployment:
|
|
|
95
104
|
|
|
96
105
|
Fingerprinting assumes files on disk are stable and requires `watch: false`.
|
|
97
106
|
|
|
107
|
+
## Browser HMR
|
|
108
|
+
|
|
109
|
+
Use browser HMR when source-served browser modules should update without a full page reload during development. Let `remix/node-hmr` own the browser HMR channel so browser updates stay coordinated with server restarts.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { createAssetServer } from 'remix/assets'
|
|
113
|
+
import { uiHmr } from 'remix/ui-hmr/assets'
|
|
114
|
+
|
|
115
|
+
const isDevelopment = process.env.NODE_ENV === 'development'
|
|
116
|
+
const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
|
|
117
|
+
|
|
118
|
+
const assetServer = createAssetServer({
|
|
119
|
+
basePath: '/assets',
|
|
120
|
+
fileMap: { '/app/*path': 'app/*path' },
|
|
121
|
+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
122
|
+
denyFiles: ['app/**/*.test.*'],
|
|
123
|
+
watch: isDevelopment,
|
|
124
|
+
hmr: isHmr
|
|
125
|
+
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
|
|
126
|
+
: undefined,
|
|
127
|
+
scripts: {
|
|
128
|
+
loaders: isHmr ? [uiHmr()] : undefined,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Rules:
|
|
134
|
+
|
|
135
|
+
- Guard `remix/node-hmr/runtime` imports with `process.env.REMIX_NODE_HMR`; that runtime API is only available inside the supervised child process.
|
|
136
|
+
- Keep browser HMR and loaders development-only.
|
|
137
|
+
- Add `remix/assets/types/hmr` to `compilerOptions.types` only when browser source modules use `import.meta.hot` directly.
|
|
138
|
+
- Write HMR accept calls directly as `import.meta.hot.accept(...)` with literal dependency specifiers.
|
|
139
|
+
|
|
98
140
|
## Useful Compiler Options
|
|
99
141
|
|
|
100
142
|
- `minify` for production minification of scripts and styles
|
|
@@ -103,6 +145,7 @@ Fingerprinting assumes files on disk are stable and requires `watch: false`.
|
|
|
103
145
|
- `target` as an object for shared browser targets and script-only ECMAScript output, such as `{ es: '2020', chrome: '109', safari: '16.4' }`
|
|
104
146
|
- `scripts.define` to replace globals such as `process.env.NODE_ENV`
|
|
105
147
|
- `scripts.external` to leave specific script imports untouched
|
|
148
|
+
- `scripts.loaders` to transform browser modules during compilation
|
|
106
149
|
|
|
107
150
|
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.
|
|
108
151
|
|
|
@@ -137,7 +137,7 @@ Notice that there is no manual `Set-Cookie` plumbing in the action — the sessi
|
|
|
137
137
|
```typescript
|
|
138
138
|
import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
|
|
139
139
|
import { Session } from 'remix/session'
|
|
140
|
-
import {
|
|
140
|
+
import { databaseContext } from '~/middleware/database.ts'
|
|
141
141
|
|
|
142
142
|
export function loadAuth() {
|
|
143
143
|
return auth({
|
|
@@ -148,7 +148,7 @@ export function loadAuth() {
|
|
|
148
148
|
return data ?? null
|
|
149
149
|
},
|
|
150
150
|
async verify(value, context) {
|
|
151
|
-
let db = context.get(
|
|
151
|
+
let db = context.get(databaseContext)
|
|
152
152
|
return (await db.find(users, value.userId)) ?? null
|
|
153
153
|
},
|
|
154
154
|
invalidate(session) {
|
|
@@ -195,7 +195,7 @@ export let passwordProvider = createCredentialsAuthProvider({
|
|
|
195
195
|
return s.parse(loginSchema, formData)
|
|
196
196
|
},
|
|
197
197
|
async verify({ email, password }, context) {
|
|
198
|
-
let db = context.get(
|
|
198
|
+
let db = context.get(databaseContext)
|
|
199
199
|
let user = await db.findOne(users, { where: { email } })
|
|
200
200
|
if (!user || !(await verifyPassword(password, user.password_hash))) {
|
|
201
201
|
return null
|
|
@@ -300,7 +300,7 @@ export default createController(routes.auth.google, {
|
|
|
300
300
|
async callback(context) {
|
|
301
301
|
let { result, returnTo } = await finishExternalAuth(googleProvider, context)
|
|
302
302
|
|
|
303
|
-
let db = context.get(
|
|
303
|
+
let db = context.get(databaseContext)
|
|
304
304
|
let { user, authAccount } = await resolveExternalAuth(db, result)
|
|
305
305
|
|
|
306
306
|
let session = completeAuth(context)
|
|
@@ -322,7 +322,7 @@ Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC to
|
|
|
322
322
|
|
|
323
323
|
```typescript
|
|
324
324
|
async function refreshGoogleTokens({ get }) {
|
|
325
|
-
let db = get(
|
|
325
|
+
let db = get(databaseContext)
|
|
326
326
|
let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
|
|
327
327
|
if (!account) return null
|
|
328
328
|
|
|
@@ -113,30 +113,30 @@ export const books = table({
|
|
|
113
113
|
|
|
114
114
|
## Database Setup
|
|
115
115
|
|
|
116
|
-
Create a database
|
|
116
|
+
Create a database and expose it via middleware:
|
|
117
117
|
|
|
118
118
|
```typescript
|
|
119
119
|
import BetterSqlite3 from 'better-sqlite3'
|
|
120
|
-
import {
|
|
121
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
120
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
122
121
|
|
|
123
122
|
let sqlite = new BetterSqlite3('./db/app.db')
|
|
124
123
|
sqlite.pragma('foreign_keys = ON')
|
|
125
|
-
let
|
|
126
|
-
export let db = createDatabase(adapter)
|
|
124
|
+
export let db = createSqliteDatabase(sqlite)
|
|
127
125
|
```
|
|
128
126
|
|
|
129
|
-
`
|
|
127
|
+
`createSqliteDatabase` 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.
|
|
130
128
|
|
|
131
129
|
### Database middleware
|
|
132
130
|
|
|
133
131
|
```typescript
|
|
134
|
-
import type {
|
|
135
|
-
import {
|
|
132
|
+
import type { Database } from 'remix/data-table'
|
|
133
|
+
import { createContextKey, type Middleware } from 'remix/router'
|
|
134
|
+
|
|
135
|
+
export const databaseContext = createContextKey<Database>()
|
|
136
136
|
|
|
137
137
|
export function loadDatabase(): Middleware {
|
|
138
138
|
return async (context, next) => {
|
|
139
|
-
context.set(
|
|
139
|
+
context.set(databaseContext, db)
|
|
140
140
|
return next()
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -145,7 +145,7 @@ export function loadDatabase(): Middleware {
|
|
|
145
145
|
### Querying
|
|
146
146
|
|
|
147
147
|
```typescript
|
|
148
|
-
let db = get(
|
|
148
|
+
let db = get(databaseContext)
|
|
149
149
|
|
|
150
150
|
// Find by primary key
|
|
151
151
|
let book = await db.find(books, id)
|
|
@@ -220,7 +220,7 @@ Do **not** import app code (e.g. `app/data/schema.ts`) into migration files. Mig
|
|
|
220
220
|
|
|
221
221
|
### Transaction modes
|
|
222
222
|
|
|
223
|
-
Migrations run inside a transaction by default (when the
|
|
223
|
+
Migrations run inside a transaction by default (when the database supports transactional DDL). Override per migration with a directive comment in `up.sql`:
|
|
224
224
|
|
|
225
225
|
```sql
|
|
226
226
|
-- data-table/transaction: none
|
|
@@ -232,15 +232,15 @@ Modes: `auto` (default — wrap when supported), `required` (wrap; throw if unsu
|
|
|
232
232
|
### Running migrations
|
|
233
233
|
|
|
234
234
|
```typescript
|
|
235
|
-
import { createMigrationRunner } from 'remix/data-table/migrations'
|
|
236
235
|
import { loadMigrations } from 'remix/data-table/migrations/node'
|
|
237
236
|
|
|
238
237
|
let migrations = await loadMigrations('./db/migrations')
|
|
239
|
-
|
|
240
|
-
await runner.up()
|
|
238
|
+
await db.migrate(migrations)
|
|
241
239
|
```
|
|
242
240
|
|
|
243
|
-
The
|
|
241
|
+
The database checksums each `up.sql` and detects drift if a previously applied migration changes.
|
|
242
|
+
Use `db.migrationStatus(migrations)` to inspect applied/pending/drifted state, and
|
|
243
|
+
`db.migrate(migrations, { direction: 'down' })` to revert.
|
|
244
244
|
|
|
245
245
|
## Input Validation (`remix/data-schema`)
|
|
246
246
|
|
|
@@ -7,11 +7,12 @@ How server-rendered UI becomes interactive in the browser, and how the page upda
|
|
|
7
7
|
- Marking a component for client-side hydration with `clientEntry`
|
|
8
8
|
- Booting the client runtime with `run`
|
|
9
9
|
- Streaming server content into a region of the page with `<Frame>` and reloading those regions
|
|
10
|
+
- Handling browser HMR updates for hydrated entries
|
|
10
11
|
- Triggering Navigation API transitions with `navigate(...)` or `link(...)`
|
|
11
12
|
- Server rendering with `renderToStream` or `renderToString`
|
|
12
13
|
- Managing the document `<head>`
|
|
13
14
|
|
|
14
|
-
For component-local state and updates, see `component-model.md`. For host-element behavior and events, see `mixins-styling-events.md`.
|
|
15
|
+
For component-local state and updates, see `component-model.md`. For host-element behavior and events, see `mixins-styling-events.md`. For browser asset HMR setup, see `assets-and-browser-modules.md`.
|
|
15
16
|
|
|
16
17
|
## Server First, Then Hydrate
|
|
17
18
|
|
|
@@ -94,21 +95,39 @@ Client entry props must be serializable: strings, numbers, booleans, `null`, `un
|
|
|
94
95
|
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
|
|
95
96
|
|
|
96
97
|
```tsx
|
|
98
|
+
import type { ResolveFrameOptions } from 'remix/ui'
|
|
97
99
|
import { run } from 'remix/ui'
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
const app = run({
|
|
100
102
|
async loadModule(moduleUrl, exportName) {
|
|
101
103
|
let mod = await import(moduleUrl)
|
|
102
104
|
return mod[exportName]
|
|
103
105
|
},
|
|
104
|
-
async resolveFrame(src,
|
|
105
|
-
let headers = new Headers({ accept: 'text/html' })
|
|
106
|
-
if (target) headers.set('x-remix-target', target)
|
|
107
|
-
let response = await fetch(src, {
|
|
106
|
+
async resolveFrame(src, options) {
|
|
107
|
+
let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
|
|
108
|
+
if (options?.target) headers.set('x-remix-target', options.target)
|
|
109
|
+
let response = await fetch(src, {
|
|
110
|
+
body: getRequestBody(options),
|
|
111
|
+
headers,
|
|
112
|
+
method: options?.method,
|
|
113
|
+
signal: options?.signal,
|
|
114
|
+
})
|
|
108
115
|
return response.body ?? (await response.text())
|
|
109
116
|
},
|
|
110
117
|
})
|
|
111
118
|
|
|
119
|
+
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
|
|
120
|
+
let formData = options?.formData
|
|
121
|
+
if (!formData) return
|
|
122
|
+
if (options.encType !== 'application/x-www-form-urlencoded') return formData
|
|
123
|
+
|
|
124
|
+
let body = new URLSearchParams()
|
|
125
|
+
for (let [name, value] of formData) {
|
|
126
|
+
body.append(name, typeof value === 'string' ? value : value.name)
|
|
127
|
+
}
|
|
128
|
+
return body
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
app.addEventListener('error', (event) => {
|
|
113
132
|
console.error('Component error:', event.error)
|
|
114
133
|
})
|
|
@@ -119,7 +138,7 @@ await app.ready()
|
|
|
119
138
|
### `run` options
|
|
120
139
|
|
|
121
140
|
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
|
|
122
|
-
- **`resolveFrame(src,
|
|
141
|
+
- **`resolveFrame(src, options)`** (optional) — called when a `<Frame>` loads or reloads content and for intercepted link and form navigations. `options` may contain `signal` and `target`; non-GET forms also provide `formData`, `method`, and `encType`.
|
|
123
142
|
|
|
124
143
|
### `app` methods
|
|
125
144
|
|
|
@@ -129,6 +148,19 @@ await app.ready()
|
|
|
129
148
|
|
|
130
149
|
`app` is an `EventTarget` that emits `error` events from any hydrated component.
|
|
131
150
|
|
|
151
|
+
## Browser HMR Updates
|
|
152
|
+
|
|
153
|
+
When `remix/node-hmr` reports a server update, reload the top frame to apply the latest server-rendered document while preserving browser state:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
if (import.meta.hot) {
|
|
157
|
+
import.meta.hot.on('server:update', async () => {
|
|
158
|
+
await app.ready()
|
|
159
|
+
await app.frames.top.reload()
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
132
164
|
## Frames
|
|
133
165
|
|
|
134
166
|
A `<Frame>` renders server content into the page. Frames stream after the initial HTML, nest inside other frames, contain client entries, and can be reloaded without full page navigation.
|
|
@@ -175,6 +207,20 @@ handle.frames.top.reload()
|
|
|
175
207
|
|
|
176
208
|
When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state.
|
|
177
209
|
|
|
210
|
+
### Form navigation
|
|
211
|
+
|
|
212
|
+
When `run({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
|
|
213
|
+
|
|
214
|
+
- Forms target `handle.frames.top` by default.
|
|
215
|
+
- `rmx-target` selects a named frame.
|
|
216
|
+
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
|
|
217
|
+
- `rmx-history="push|replace"` overrides how the navigation updates history.
|
|
218
|
+
- `rmx-reset-scroll="false"` preserves scroll position.
|
|
219
|
+
- `rmx-document` opts back into a document submission.
|
|
220
|
+
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
|
|
221
|
+
|
|
222
|
+
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
|
|
223
|
+
|
|
178
224
|
### Nested frames
|
|
179
225
|
|
|
180
226
|
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
|
|
@@ -238,7 +284,7 @@ navigate('/dashboard', { history: 'replace' })
|
|
|
238
284
|
|
|
239
285
|
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
|
|
240
286
|
|
|
241
|
-
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`.
|
|
287
|
+
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
|
|
242
288
|
|
|
243
289
|
## Head Management
|
|
244
290
|
|