@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
|
@@ -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 {
|
|
122
|
+
import { databaseContext } from '~/middleware/database.ts'
|
|
120
123
|
|
|
121
124
|
export function loadDatabase(): Middleware {
|
|
122
125
|
return async (context, next) => {
|
|
123
|
-
context.set(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 `
|
|
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
|
|
package/template/AGENTS.md
CHANGED
|
@@ -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/
|
|
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
|
|
package/template/README.md
CHANGED
|
@@ -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/
|
|
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 '
|
|
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 {
|
|
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
|
-
|
|
90
|
-
|
|
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',
|
package/template/app/assets.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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)
|
package/template/hmr.ts
ADDED
|
@@ -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)
|
package/template/package.json
CHANGED
|
@@ -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"
|
package/template/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/template/tsconfig.json
CHANGED
|
@@ -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"}
|