@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.
Files changed (118) hide show
  1. package/README.md +105 -1
  2. package/dist/index.js +1 -1
  3. package/dist/lib/app-root.d.ts +11 -0
  4. package/dist/lib/app-root.d.ts.map +1 -0
  5. package/dist/lib/app-root.js +38 -0
  6. package/dist/lib/bootstrap-project.js +2 -2
  7. package/dist/lib/cli-context.d.ts +6 -0
  8. package/dist/lib/cli-context.d.ts.map +1 -1
  9. package/dist/lib/cli-context.js +12 -1
  10. package/dist/lib/cli.d.ts +3 -2
  11. package/dist/lib/cli.d.ts.map +1 -1
  12. package/dist/lib/cli.js +42 -14
  13. package/dist/lib/commands/completion.js +4 -4
  14. package/dist/lib/commands/db.d.ts +4 -0
  15. package/dist/lib/commands/db.d.ts.map +1 -0
  16. package/dist/lib/commands/db.js +283 -0
  17. package/dist/lib/commands/doctor.d.ts +5 -0
  18. package/dist/lib/commands/doctor.d.ts.map +1 -1
  19. package/dist/lib/commands/doctor.js +26 -251
  20. package/dist/lib/commands/help.d.ts.map +1 -1
  21. package/dist/lib/commands/help.js +17 -9
  22. package/dist/lib/commands/new.js +6 -6
  23. package/dist/lib/commands/routes.js +6 -6
  24. package/dist/lib/commands/test.d.ts +20 -1
  25. package/dist/lib/commands/test.d.ts.map +1 -1
  26. package/dist/lib/commands/test.js +353 -10
  27. package/dist/lib/commands/version.js +3 -3
  28. package/dist/lib/completion.d.ts +1 -1
  29. package/dist/lib/completion.d.ts.map +1 -1
  30. package/dist/lib/completion.js +229 -6
  31. package/dist/lib/controller-files.d.ts +0 -8
  32. package/dist/lib/controller-files.d.ts.map +1 -1
  33. package/dist/lib/controller-files.js +0 -23
  34. package/dist/lib/controller-ownership.d.ts +4 -12
  35. package/dist/lib/controller-ownership.d.ts.map +1 -1
  36. package/dist/lib/controller-ownership.js +31 -127
  37. package/dist/lib/database-command.d.ts +20 -0
  38. package/dist/lib/database-command.d.ts.map +1 -0
  39. package/dist/lib/database-command.js +7 -0
  40. package/dist/lib/doctor/controller-findings.d.ts +2 -2
  41. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  42. package/dist/lib/doctor/controller-findings.js +23 -39
  43. package/dist/lib/doctor/controllers.d.ts +1 -2
  44. package/dist/lib/doctor/controllers.d.ts.map +1 -1
  45. package/dist/lib/doctor/controllers.js +4 -7
  46. package/dist/lib/doctor/environment.js +2 -2
  47. package/dist/lib/doctor/fixes.js +1 -1
  48. package/dist/lib/doctor/project.d.ts.map +1 -1
  49. package/dist/lib/doctor/project.js +3 -212
  50. package/dist/lib/doctor/run.d.ts +9 -0
  51. package/dist/lib/doctor/run.d.ts.map +1 -0
  52. package/dist/lib/doctor/run.js +237 -0
  53. package/dist/lib/doctor/types.d.ts +2 -1
  54. package/dist/lib/doctor/types.d.ts.map +1 -1
  55. package/dist/lib/doctor/types.js +4 -1
  56. package/dist/lib/errors.d.ts +30 -0
  57. package/dist/lib/errors.d.ts.map +1 -1
  58. package/dist/lib/errors.js +55 -0
  59. package/dist/lib/help-text.js +1 -1
  60. package/dist/lib/parse-args.d.ts.map +1 -1
  61. package/dist/lib/parse-args.js +17 -4
  62. package/dist/lib/remix-config.d.ts +75 -0
  63. package/dist/lib/remix-config.d.ts.map +1 -0
  64. package/dist/lib/remix-config.js +411 -0
  65. package/dist/lib/reporter.js +1 -1
  66. package/dist/lib/route-map.js +13 -33
  67. package/package.json +13 -7
  68. package/schema/remix.json +242 -0
  69. package/src/lib/app-root.ts +46 -0
  70. package/src/lib/cli-context.ts +17 -0
  71. package/src/lib/cli.ts +41 -6
  72. package/src/lib/commands/db.ts +377 -0
  73. package/src/lib/commands/doctor.ts +27 -329
  74. package/src/lib/commands/help.ts +10 -1
  75. package/src/lib/commands/test.ts +453 -12
  76. package/src/lib/completion.ts +321 -6
  77. package/src/lib/controller-files.ts +0 -38
  78. package/src/lib/controller-ownership.ts +50 -203
  79. package/src/lib/database-command.ts +31 -0
  80. package/src/lib/doctor/controller-findings.ts +30 -47
  81. package/src/lib/doctor/controllers.ts +2 -6
  82. package/src/lib/doctor/project.ts +0 -236
  83. package/src/lib/doctor/run.ts +311 -0
  84. package/src/lib/doctor/types.ts +6 -5
  85. package/src/lib/errors.ts +65 -0
  86. package/src/lib/parse-args.ts +19 -3
  87. package/src/lib/remix-config.ts +639 -0
  88. package/src/lib/route-map.ts +11 -48
  89. package/template/.agents/skills/remix/SKILL.md +38 -27
  90. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
  91. package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
  92. package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
  93. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
  94. package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
  95. package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
  96. package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
  97. package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
  98. package/template/AGENTS.md +6 -3
  99. package/template/README.md +6 -3
  100. package/template/app/actions/controller.tsx +1 -1
  101. package/template/app/{ui → actions}/document.tsx +7 -5
  102. package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
  103. package/template/app/actions/public/entry.ts +48 -0
  104. package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
  105. package/template/app/assets.ts +15 -2
  106. package/template/hmr.ts +46 -0
  107. package/template/package.json +1 -0
  108. package/template/server.ts +8 -1
  109. package/template/tsconfig.json +1 -1
  110. package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
  111. package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
  112. package/dist/lib/doctor/controller-fix-plans.js +0 -119
  113. package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
  114. package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
  115. package/dist/lib/doctor/controller-placeholders.js +0 -52
  116. package/src/lib/doctor/controller-fix-plans.ts +0 -168
  117. package/src/lib/doctor/controller-placeholders.ts +0 -70
  118. package/template/app/assets/entry.ts +0 -17
@@ -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 RouteDirectoryPlan,
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, RouteDirectoryPlan>,
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, RouteDirectoryPlan>,
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: actualRouteDirectories.has(directory.directoryPath),
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 currentDir = path.resolve(startDir)
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
- let parentDir = path.dirname(currentDir)
307
- if (parentDir === currentDir) {
308
- break
309
- }
310
-
311
- currentDir = parentDir
287
+ if (appRoot == null) {
288
+ throw routesFileNotFound(startDir)
312
289
  }
313
290
 
314
- throw routesFileNotFound(startDir)
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... | 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 | `references/middleware-and-server.md` |
49
- | Compiling and serving browser modules, asset URL namespaces, preloads | `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, `<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` |
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(Database)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
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/assets` — browser asset server. Use for `createAssetServer` when serving compiled scripts and styles, getting public hrefs, and emitting preloads. Configure a `basePath`, and keep `fileMap` URL patterns relative to it. Shared compiler options such as `target`, `sourceMaps`, `sourceMapSourcePaths`, and `minify` live at the top level
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 a `Database` interface. Use for `table`, `column`, `createDatabase` when modeling persisted data
226
- - `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — adapters. Use to back `createDatabase` with a real engine. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
227
- - `remix/data-table/migrations` — migration authoring and runners. Use for `createMigration`, `createMigrationRunner`
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 adapter or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
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(Database)
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(Database)
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(Database)
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`, `allow`, `deny`, fingerprinting, compiler options)
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
- - Keeping server-only files out of the browser via `deny` rules
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 under `app/assets/`, and monorepo code that should be compiled and served under a public URL namespace.
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
- allow: ['app/assets/**', 'node_modules/**'],
39
- deny: ['app/**/*.server.*'],
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 `allow` and `deny` as the security boundary for browser-reachable source files.
62
- - Add a `deny` list for server-only modules such as `*.server.*`, private config, or other files that should never be exposed.
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/assets/entry.ts')
75
- let preloads = await assetServer.getPreloads(['app/assets/entry.ts'])
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 { Database } from 'remix/data-table'
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(Database)
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(Database)
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(Database)
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(Database)
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 with an adapter and expose it via middleware:
116
+ Create a database and expose it via middleware:
117
117
 
118
118
  ```typescript
119
119
  import BetterSqlite3 from 'better-sqlite3'
120
- import { createDatabase, Database } from 'remix/data-table'
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 adapter = createSqliteDatabaseAdapter(sqlite)
126
- export let db = createDatabase(adapter)
124
+ export let db = createSqliteDatabase(sqlite)
127
125
  ```
128
126
 
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.
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 { Middleware } from 'remix/router'
135
- import { Database } from 'remix/data-table'
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(Database, db)
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(Database)
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 adapter supports transactional DDL). Override per migration with a directive comment in `up.sql`:
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
- let runner = createMigrationRunner(adapter, migrations)
240
- await runner.up()
238
+ await db.migrate(migrations)
241
239
  ```
242
240
 
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.
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
- let app = run({
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, signal, target) {
105
- let headers = new Headers({ accept: 'text/html' })
106
- if (target) headers.set('x-remix-target', target)
107
- let response = await fetch(src, { headers, signal })
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, signal, target)`** (optional) — called when a `<Frame>` loads or reloads content. `target` is available when frame targeting matters.
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