@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
@@ -8,8 +8,9 @@ How to compose the request lifecycle and bridge the router to a runtime. Read th
8
8
  - Writing custom middleware that sets typed context values
9
9
  - Adding fast-exit handling (static files, CORS preflights) versus request-enriching layers (sessions, auth, data loading)
10
10
  - Choosing when to keep the generated Node server versus switching server adapters
11
+ - Running the app server under development HMR
11
12
 
12
- For data and persistence specifics, see `data-and-validation.md`. For session and auth specifics, see `auth-and-sessions.md`.
13
+ For data and persistence specifics, see `data-and-validation.md`. For session and auth specifics, see `auth-and-sessions.md`. For browser asset HMR, see `assets-and-browser-modules.md`.
13
14
 
14
15
  ## Middleware Stack
15
16
 
@@ -64,8 +65,9 @@ let router = createRouter({ middleware })
64
65
 
65
66
  ### Static files vs browser modules
66
67
 
67
- - Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in `public/`
68
+ - Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
68
69
  - Use `remix/assets` when browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs
70
+ - `public/` directories inside `app/` hold browser-reachable source for the asset server
69
71
 
70
72
  ### Ordering notes
71
73
 
@@ -80,6 +82,7 @@ let router = createRouter({ middleware })
80
82
  - **Session-backed HTML app** -> `compression()`, `staticFiles()`, optional `cop()`, `formData()`, `methodOverride()`, `session()`, optional `csrf()`, `asyncContext()`, `auth({ schemes })`
81
83
  - **Cross-origin API** -> `compression()`, `cors()`, optional `asyncContext()`, optional `auth({ schemes })`
82
84
  - **Upload flow** -> `compression()`, `staticFiles()`, `formData({ uploadHandler })`, then sessions, auth, and data-loading middleware as needed
85
+ - **Optional development HMR** -> keep `server.ts` as the child app server, add `hmr.ts` for `remix/node-hmr`, and proxy public requests through `createHmrReadyFetch()`
83
86
 
84
87
  ### Middleware with options
85
88
 
@@ -116,11 +119,11 @@ Use `context.set(key, value)` to add typed values accessible downstream via `con
116
119
 
117
120
  ```typescript
118
121
  import type { Middleware } from 'remix/router'
119
- import { Database } from 'remix/data-table'
122
+ import { databaseContext } from '~/middleware/database.ts'
120
123
 
121
124
  export function loadDatabase(): Middleware {
122
125
  return async (context, next) => {
123
- context.set(Database, db)
126
+ context.set(databaseContext, db)
124
127
  return next()
125
128
  }
126
129
  }
@@ -150,11 +153,11 @@ export function requireAdmin(): Middleware {
150
153
  // app/utils/context.ts
151
154
  import { getContext } from 'remix/middleware/async-context'
152
155
  import { Auth } from 'remix/middleware/auth'
153
- import { Database } from 'remix/data-table'
156
+ import { databaseContext } from '~/middleware/database.ts'
154
157
  import { Session } from 'remix/session'
155
158
 
156
159
  export function getCurrentDb() {
157
- return getContext().get(Database)
160
+ return getContext().get(databaseContext)
158
161
  }
159
162
 
160
163
  export function getCurrentSession() {
@@ -214,3 +217,62 @@ Prefer inline arrays for `middleware` options. Use `RouterContext<typeof router>
214
217
  New apps already include a `server.ts` that adapts the app router with `remix/node-fetch-server`. Keep that generated server unless the task specifically needs to change runtime behavior such as host/protocol handling, TLS, HTTP/2, WebSockets, deployment lifecycle, or test-only server setup.
215
218
 
216
219
  Use `remix/node-fetch-server` when you want to keep owning a standard Node `http`, `https`, or `http2` server directly.
220
+
221
+ ## Development HMR
222
+
223
+ Treat HMR as an optional mode for rapid UI edits. Keep HMR supervision out of normal app code:
224
+ put the real app server in `server.ts`, keep the `dev` script running it directly (usually with
225
+ Node's watch mode), then add a development-only `hmr.ts` behind an `hmr` script when the project
226
+ benefits from HMR.
227
+
228
+ ```typescript
229
+ // hmr.ts
230
+ import * as http from 'node:http'
231
+
232
+ import { createFetchProxy } from 'remix/fetch-proxy'
233
+ import { createHmrReadyFetch, run } from 'remix/node-hmr'
234
+ import { createRequestListener } from 'remix/node-fetch-server'
235
+
236
+ const hmrProxyPort = 44100
237
+ const hmrEventPort = 44101
238
+ const appPort = 44102
239
+
240
+ const hmrRunner = run('./server.ts', {
241
+ env: {
242
+ ...process.env,
243
+ PORT: String(appPort),
244
+ HMR_PROXY_PORT: String(hmrProxyPort),
245
+ },
246
+ nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
247
+ browserHmrChannel: { port: hmrEventPort },
248
+ })
249
+
250
+ let proxyFetch = createFetchProxy(`http://127.0.0.1:${appPort}`, {
251
+ xForwardedHeaders: true,
252
+ })
253
+
254
+ let server = http.createServer(createRequestListener(createHmrReadyFetch(hmrRunner, proxyFetch)))
255
+
256
+ server.listen(hmrProxyPort, '127.0.0.1')
257
+ ```
258
+
259
+ Keep `browserHmrChannel.port` stable so browser HMR clients can reconnect to the same event channel if the dev server is manually restarted.
260
+
261
+ Use a stable public proxy when browser requests may happen while the child server is restarting. `createHmrReadyFetch()` waits for the active child generation before forwarding requests and retries safe unavailable responses when the child changes during a request.
262
+
263
+ In the child `server.ts`, report readiness after the server is listening:
264
+
265
+ ```typescript
266
+ server.listen(port, () => {
267
+ if (process.env.REMIX_NODE_HMR) {
268
+ import('remix/node-hmr/runtime').then((nodeHmr) => nodeHmr.emitServerReady())
269
+ }
270
+ })
271
+ ```
272
+
273
+ Rules:
274
+
275
+ - Guard `remix/node-hmr/runtime` imports with `process.env.REMIX_NODE_HMR`.
276
+ - Use `--import remix/ui-hmr/node` only when server-rendered Remix UI component modules should hot update.
277
+ - Keep the default development and production startup paths independent from `hmr.ts`.
278
+ - Close the public server and `hmrRunner` during `SIGINT` and `SIGTERM` shutdown.
@@ -115,6 +115,8 @@ Adds client-side navigation behavior to any element. Makes non-anchor elements b
115
115
 
116
116
  Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
117
117
 
118
+ On a native anchor, the `history` option renders the corresponding `rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
119
+
118
120
  ## Native press and keyboard interactions
119
121
 
120
122
  Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics:
@@ -103,7 +103,7 @@ export const search = createAction(routes.search, {
103
103
 
104
104
  The handler receives a context object with:
105
105
 
106
- - `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`)
106
+ - `get(key)` — read a value set by middleware (e.g. `get(databaseContext)`, `get(Session)`, `get(Auth)`)
107
107
  - `params` — typed route params
108
108
  - `url` — the request URL
109
109
  - `request` — the raw `Request`
@@ -132,7 +132,7 @@ For pages, render a component tree and return the resulting `Response`:
132
132
 
133
133
  ```typescript
134
134
  async handler({ get }) {
135
- let db = get(Database)
135
+ let db = get(databaseContext)
136
136
  let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
137
137
  return render(<IndexPage books={books} />)
138
138
  }
@@ -152,7 +152,7 @@ async create({ get }) {
152
152
  return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
153
153
  }
154
154
 
155
- let db = get(Database)
155
+ let db = get(databaseContext)
156
156
  let book = await db.create(books, parsed.value)
157
157
 
158
158
  return redirect(routes.books.show.href({ slug: book.slug }), 303)
@@ -167,7 +167,7 @@ For expected failures — validation, conflict, not found — return a `Response
167
167
 
168
168
  ```typescript
169
169
  async show({ get, params }) {
170
- let db = get(Database)
170
+ let db = get(databaseContext)
171
171
  let book = await db.find(books, params.bookId)
172
172
  if (!book) return new Response('Not Found', { status: 404 })
173
173
  return render(<ShowPage book={book} />)
@@ -208,7 +208,7 @@ If you find yourself returning JSON for what is really a browser form submission
208
208
 
209
209
  A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
210
210
 
211
- Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
211
+ Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(databaseContext)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
212
212
 
213
213
  ```typescript
214
214
  import { createController } from 'remix/router'
@@ -218,13 +218,13 @@ import { routes } from '../routes.ts'
218
218
  export default createController(routes.books, {
219
219
  actions: {
220
220
  async index({ get }) {
221
- let db = get(Database)
221
+ let db = get(databaseContext)
222
222
  let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
223
223
  return render(<IndexPage items={items} />)
224
224
  },
225
225
 
226
226
  async show({ get, params }) {
227
- let db = get(Database)
227
+ let db = get(databaseContext)
228
228
  let book = await db.find(books, params.bookId)
229
229
  if (!book) return new Response('Not Found', { status: 404 })
230
230
  return render(<ShowPage book={book} />)
@@ -359,4 +359,4 @@ declare module 'remix/router' {
359
359
  }
360
360
  ```
361
361
 
362
- This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.
362
+ This gives typed `context.get(databaseContext)`, `context.get(Session)`, `context.get(Auth)`, etc.
@@ -70,28 +70,29 @@ try {
70
70
 
71
71
  ## Test Runner Config
72
72
 
73
- Configure discovery and coverage in `remix-test.config.ts` or with CLI flags:
74
-
75
- ```ts
76
- export default {
77
- glob: {
78
- test: '**/*.test{,.e2e}.{ts,tsx}',
79
- e2e: '**/*.test.e2e.{ts,tsx}',
80
- exclude: 'node_modules/**',
81
- },
82
- coverage: {
83
- dir: '.coverage',
84
- include: ['app/**/*.{ts,tsx}'],
85
- exclude: ['app/**/*.test.{ts,tsx}'],
86
- statements: 80,
87
- lines: 80,
88
- branches: 70,
89
- functions: 80,
73
+ Configure discovery and coverage in the `test` section of `remix.json` or with CLI flags:
74
+
75
+ ```jsonc
76
+ {
77
+ "$schema": "https://remix.run/schemas/remix.json",
78
+ "test": {
79
+ "files": ["**/*.test{,.e2e}.{ts,tsx}"],
80
+ "e2eFiles": ["**/*.test.e2e.{ts,tsx}"],
81
+ "exclude": ["node_modules/**"],
82
+ "coverage": {
83
+ "dir": ".coverage",
84
+ "include": ["app/**/*.{ts,tsx}"],
85
+ "exclude": ["app/**/*.test.{ts,tsx}"],
86
+ "statements": 80,
87
+ "lines": 80,
88
+ "branches": 70,
89
+ "functions": 80,
90
+ },
90
91
  },
91
92
  }
92
93
  ```
93
94
 
94
- Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests.
95
+ Use `remix test --coverage` to enable coverage with defaults. Use `exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests.
95
96
 
96
97
  ## Component Tests
97
98
 
@@ -6,6 +6,8 @@ This app was scaffolded with `remix new`. Use these conventions when continuing
6
6
 
7
7
  ```sh
8
8
  npm i
9
+ npm run dev
10
+ npm run hmr
9
11
  npm run start
10
12
  npm test
11
13
  npm run typecheck
@@ -18,12 +20,13 @@ Refer to ./.agents/skills/remix/SKILL.md
18
20
  ## Starter Layout
19
21
 
20
22
  - `app/actions/controller.tsx` owns the top-level route actions
21
- - `app/routes.ts` defines the route contract
23
+ - `app/actions/home-page.tsx` and `app/actions/document.tsx` render the route-owned starter UI
24
+ - `app/actions/public/` contains the browser runtime entry and interactive prompt button
25
+ - `app/routes.ts` defines the shared route contract used by server and browser modules for type-safe hrefs
22
26
  - `app/router.ts` wires routes to route handlers
23
27
  - `app/middleware/render.tsx` installs the request-scoped renderer used by actions
24
- - `app/ui/` holds the shared document shell and home page UI
25
28
  - `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer
26
- - `public/` contains static files served from the app root
29
+ - Root `public/` contains static files served unchanged from the app root
27
30
 
28
31
  ## Route Ownership
29
32
 
@@ -5,12 +5,13 @@ A minimal Remix application starter with a home page.
5
5
  ## Starter Shape
6
6
 
7
7
  - `app/actions/controller.tsx` owns the top-level route actions.
8
- - `app/routes.ts` defines the route contract.
8
+ - `app/actions/home-page.tsx` and `app/actions/document.tsx` render the route-owned starter UI.
9
+ - `app/actions/public/` contains the browser runtime entry and interactive prompt button.
10
+ - `app/routes.ts` defines the shared route contract used by server and browser modules for type-safe hrefs.
9
11
  - `app/router.ts` wires routes to handlers.
10
12
  - `app/middleware/render.tsx` installs the request-scoped renderer used by actions.
11
- - `app/ui/` holds the shared document shell and home page UI.
12
13
  - `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer.
13
- - `public/` contains static files served from the app root.
14
+ - Root `public/` contains static files served unchanged from the app root.
14
15
 
15
16
  ## Growing The App
16
17
 
@@ -23,6 +24,8 @@ A minimal Remix application starter with a home page.
23
24
 
24
25
  ```sh
25
26
  npm i
27
+ npm run dev
28
+ npm run hmr
26
29
  npm run start
27
30
  npm test
28
31
  npm run typecheck
@@ -2,7 +2,7 @@ import { createController } from 'remix/router'
2
2
 
3
3
  import { assetServer } from '../assets.ts'
4
4
  import { routes } from '../routes.ts'
5
- import { HomePage } from '../ui/scaffold-home-page.tsx'
5
+ import { HomePage } from './home-page.tsx'
6
6
 
7
7
  export default createController(routes, {
8
8
  actions: {
@@ -1,7 +1,7 @@
1
1
  import type { Handle, RemixNode } from 'remix/ui'
2
2
  import { css } from 'remix/ui'
3
3
 
4
- import { routes } from '../routes.ts'
4
+ import { entryHref, entryPreloads } from '../assets.ts'
5
5
 
6
6
  export interface DocumentProps {
7
7
  children?: RemixNode
@@ -20,14 +20,16 @@ export function Document(handle: Handle<DocumentProps>) {
20
20
  <head>
21
21
  <meta charSet="utf-8" />
22
22
  <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <meta name="color-scheme" content="light dark" />
23
24
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
24
25
  <title>{title}</title>
25
26
  {head}
27
+ {entryPreloads.map((href) => (
28
+ <link key={href} rel="modulepreload" href={href} />
29
+ ))}
30
+ <script type="module" src={entryHref}></script>
26
31
  </head>
27
- <body mix={css({ margin: 0 })}>
28
- {children}
29
- <script type="module" src={routes.assets.href({ path: 'app/assets/entry.ts' })}></script>
30
- </body>
32
+ <body mix={css({ margin: 0 })}>{children}</body>
31
33
  </html>
32
34
  )
33
35
  }
@@ -2,8 +2,8 @@
2
2
  import type { Handle, RemixNode } from 'remix/ui'
3
3
  import { css } from 'remix/ui'
4
4
 
5
- import { PromptButton } from '../assets/prompt-button.tsx'
6
5
  import { Document } from './document.tsx'
6
+ import { PromptButton } from './public/prompt-button.tsx'
7
7
 
8
8
  const FONT_STACK =
9
9
  "'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace"
@@ -66,7 +66,6 @@ export function HomePage() {
66
66
  function HomeHead() {
67
67
  return () => (
68
68
  <>
69
- <meta name="color-scheme" content="light dark" />
70
69
  <link rel="preconnect" href="https://fonts.googleapis.com" />
71
70
  <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
72
71
  <link
@@ -0,0 +1,48 @@
1
+ import { run } from 'remix/ui'
2
+
3
+ const app = run({
4
+ async loadModule(moduleUrl, exportName) {
5
+ let mod = await import(moduleUrl)
6
+ return mod[exportName]
7
+ },
8
+ async resolveFrame(src, options) {
9
+ let response = await fetch(src, {
10
+ headers: { Accept: 'text/html' },
11
+ method: options?.method,
12
+ body: getRequestBody(options?.formData, options?.method, options?.encType),
13
+ signal: options?.signal,
14
+ })
15
+ if (!response.ok) {
16
+ return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
17
+ }
18
+
19
+ if (response.body) return response.body
20
+ return await response.text()
21
+ },
22
+ })
23
+
24
+ if (import.meta.hot) {
25
+ import.meta.hot.on('server:update', async () => {
26
+ try {
27
+ await app.ready()
28
+ await app.frames.top.reload()
29
+ } catch (error) {
30
+ console.error('Error reloading top frame on server update', error)
31
+ }
32
+ })
33
+ }
34
+
35
+ function getRequestBody(
36
+ formData?: FormData,
37
+ method?: string,
38
+ encType?: string,
39
+ ): BodyInit | undefined {
40
+ if (!formData || method?.toLowerCase() === 'get') return
41
+ if (encType !== 'application/x-www-form-urlencoded') return formData
42
+
43
+ let body = new URLSearchParams()
44
+ for (let [name, value] of formData) {
45
+ body.append(name, typeof value === 'string' ? value : value.name)
46
+ }
47
+ return body
48
+ }
@@ -28,11 +28,6 @@ export const PromptButton = clientEntry(
28
28
  return (
29
29
  <button
30
30
  type="button"
31
- className={state}
32
- style={{
33
- background: active ? 'var(--surface-4)' : undefined,
34
- color: active ? 'var(--brand-blue)' : undefined,
35
- }}
36
31
  mix={[
37
32
  buttonStyle,
38
33
  on('click', async (_event, signal) => {
@@ -67,32 +62,23 @@ export const PromptButton = clientEntry(
67
62
  await handle.update()
68
63
  }),
69
64
  ]}
65
+ style={
66
+ active
67
+ ? {
68
+ background: 'var(--surface-4)',
69
+ color: 'var(--brand-blue)',
70
+ }
71
+ : undefined
72
+ }
70
73
  >
71
74
  <span aria-hidden="true" mix={iconSlotStyle}>
72
75
  <CopyIcon />
73
76
  </span>
74
- <span
75
- mix={css({
76
- alignItems: 'center',
77
- display: 'flex',
78
- fontSize: '14px',
79
- flex: '1 1 0',
80
- lineHeight: 1.5,
81
- minWidth: 0,
82
- position: 'relative',
83
- transition: 'opacity 180ms ease',
84
- })}
85
- style={{ opacity: state === 'resetting' ? 0 : undefined }}
86
- >
77
+ <span mix={labelSlotStyle} style={{ opacity: state === 'resetting' ? 0 : 1 }}>
87
78
  <span
88
79
  aria-hidden={state === 'idle' ? true : undefined}
89
- style={{
90
- alignItems: 'center',
91
- display: 'flex',
92
- inset: 0,
93
- position: 'absolute',
94
- visibility: state === 'idle' ? 'hidden' : 'visible',
95
- }}
80
+ mix={statusLabelStyle}
81
+ style={{ visibility: state === 'idle' ? 'hidden' : 'visible' }}
96
82
  >
97
83
  {label}
98
84
  </span>
@@ -148,6 +134,24 @@ const buttonStyle = css({
148
134
  },
149
135
  })
150
136
 
137
+ const labelSlotStyle = css({
138
+ alignItems: 'center',
139
+ display: 'flex',
140
+ fontSize: '14px',
141
+ flex: '1 1 0',
142
+ lineHeight: 1.5,
143
+ minWidth: 0,
144
+ position: 'relative',
145
+ transition: 'opacity 180ms ease',
146
+ })
147
+
148
+ const statusLabelStyle = css({
149
+ alignItems: 'center',
150
+ display: 'flex',
151
+ inset: 0,
152
+ position: 'absolute',
153
+ })
154
+
151
155
  const iconSlotStyle = css({
152
156
  flex: '0 0 24px',
153
157
  width: '24px',
@@ -1,8 +1,10 @@
1
1
  import { createAssetServer } from 'remix/assets'
2
+ import { uiHmr } from 'remix/ui-hmr/assets'
2
3
 
3
4
  const rootDir = process.cwd()
4
5
  const nodeEnv = process.env.NODE_ENV ?? 'development'
5
6
  const isDevelopment = nodeEnv === 'development'
7
+ const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
6
8
 
7
9
  export const assetServer = createAssetServer({
8
10
  basePath: '/assets',
@@ -11,8 +13,19 @@ export const assetServer = createAssetServer({
11
13
  'app/*path': 'app/*path',
12
14
  'node_modules/*path': 'node_modules/*path',
13
15
  },
14
- allow: ['app/assets/**', 'node_modules/**'],
16
+ allowFiles: ['app/routes.ts', 'app/**/public/**'],
17
+ allowPackages: ['remix'],
18
+ denyFiles: ['app/**/*.test.*'],
15
19
  sourceMaps: isDevelopment ? 'external' : undefined,
16
20
  minify: !isDevelopment,
17
- watch: false,
21
+ watch: isDevelopment,
22
+ hmr: isHmr
23
+ ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
24
+ : undefined,
25
+ scripts: { loaders: isHmr ? [uiHmr()] : undefined },
18
26
  })
27
+
28
+ const entry = 'app/actions/public/entry.ts'
29
+
30
+ export const entryHref = await assetServer.getHref(entry)
31
+ export const entryPreloads = await assetServer.getPreloads(entry)
@@ -0,0 +1,46 @@
1
+ import * as http from 'node:http'
2
+
3
+ import { createFetchProxy } from 'remix/fetch-proxy'
4
+ import { createHmrReadyFetch, run } from 'remix/node-hmr'
5
+ import { createRequestListener } from 'remix/node-fetch-server'
6
+
7
+ const hmrProxyPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 44100
8
+ const hmrEventPort = process.env.HMR_PORT
9
+ ? Number.parseInt(process.env.HMR_PORT, 10)
10
+ : hmrProxyPort + 1
11
+ const appPort = process.env.APP_PORT ? Number.parseInt(process.env.APP_PORT, 10) : hmrEventPort + 1
12
+
13
+ const hmrRunner = run('server.ts', {
14
+ env: {
15
+ ...process.env,
16
+ PORT: String(appPort),
17
+ HMR_PROXY_PORT: String(hmrProxyPort),
18
+ },
19
+ nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
20
+ browserHmrChannel: { port: hmrEventPort },
21
+ })
22
+
23
+ const server = http.createServer(
24
+ createRequestListener(
25
+ createHmrReadyFetch(
26
+ hmrRunner,
27
+ createFetchProxy(`http://127.0.0.1:${appPort}`, {
28
+ xForwardedHeaders: true,
29
+ }),
30
+ ),
31
+ ),
32
+ )
33
+
34
+ server.listen(hmrProxyPort, '127.0.0.1')
35
+
36
+ let shuttingDown = false
37
+
38
+ function shutdown() {
39
+ if (shuttingDown) return
40
+ shuttingDown = true
41
+ server.close(() => hmrRunner.close().finally(() => process.exit(0)))
42
+ server.closeAllConnections()
43
+ }
44
+
45
+ process.on('SIGINT', shutdown)
46
+ process.on('SIGTERM', shutdown)
@@ -4,6 +4,7 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "NODE_ENV=development node --watch --import remix/node-tsx server.ts",
7
+ "hmr": "NODE_ENV=development node hmr.ts",
7
8
  "start": "NODE_ENV=production node --import remix/node-tsx server.ts",
8
9
  "test": "NODE_ENV=test node --import remix/node-tsx --test",
9
10
  "typecheck": "tsc --noEmit"
@@ -4,6 +4,9 @@ import { createRequestListener } from 'remix/node-fetch-server'
4
4
  import { router } from './app/router.ts'
5
5
 
6
6
  const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 44100
7
+ const hmrProxyPort = process.env.HMR_PROXY_PORT
8
+ ? Number.parseInt(process.env.HMR_PROXY_PORT, 10)
9
+ : null
7
10
 
8
11
  const server = http.createServer(
9
12
  createRequestListener(async (request) => {
@@ -19,7 +22,11 @@ const server = http.createServer(
19
22
  )
20
23
 
21
24
  server.listen(port, () => {
22
- console.log(`Server listening on http://localhost:${port}`)
25
+ if (process.env.REMIX_NODE_HMR) {
26
+ import('remix/node-hmr/runtime').then((nodeHmr) => nodeHmr.emitServerReady())
27
+ }
28
+
29
+ console.log(`Server listening on http://localhost:${hmrProxyPort ?? port}`)
23
30
  })
24
31
 
25
32
  let shuttingDown = false
@@ -2,7 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
4
  "lib": ["ES2024", "DOM", "DOM.Iterable"],
5
- "types": ["node"],
5
+ "types": ["node", "remix/assets/types/hmr"],
6
6
  "module": "NodeNext",
7
7
  "moduleResolution": "NodeNext",
8
8
  "target": "ESNext",
@@ -1,4 +0,0 @@
1
- import type { ControllerOwnership, OwnershipRouteNode } from '../controller-ownership.ts';
2
- import type { DoctorFixPlan } from './types.ts';
3
- export declare function getControllerFixPlans(appRoot: string, tree: OwnershipRouteNode[], ownership: ControllerOwnership): Promise<DoctorFixPlan[]>;
4
- //# sourceMappingURL=controller-fix-plans.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"controller-fix-plans.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-fix-plans.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,EAC1B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAkD1B"}