@remix-run/cli 0.5.0 → 0.7.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 +52 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +5 -2
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.js +5 -1
- package/dist/lib/commands/assets.d.ts +4 -0
- package/dist/lib/commands/assets.d.ts.map +1 -0
- package/dist/lib/commands/assets.js +90 -0
- package/dist/lib/commands/db.d.ts.map +1 -1
- package/dist/lib/commands/db.js +63 -5
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +7 -0
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +25 -1
- package/dist/lib/database-command.d.ts +5 -1
- package/dist/lib/database-command.d.ts.map +1 -1
- package/dist/lib/database-command.js +1 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +10 -0
- package/dist/lib/remix-config.d.ts +22 -0
- package/dist/lib/remix-config.d.ts.map +1 -1
- package/dist/lib/remix-config.js +80 -4
- package/package.json +6 -5
- package/schema/remix.json +49 -1
- package/src/index.ts +10 -0
- package/src/lib/cli-context.ts +10 -2
- package/src/lib/cli.ts +6 -1
- package/src/lib/commands/assets.ts +109 -0
- package/src/lib/commands/db.ts +75 -5
- package/src/lib/commands/help.ts +8 -0
- package/src/lib/completion.ts +41 -1
- package/src/lib/database-command.ts +6 -1
- package/src/lib/errors.ts +11 -0
- package/src/lib/remix-config.ts +128 -4
- package/template/.agents/skills/remix/SKILL.md +7 -5
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +22 -20
- package/template/.agents/skills/remix/references/auth-and-sessions.md +1 -15
- package/template/.agents/skills/remix/references/component-model.md +18 -16
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +75 -52
- package/template/.agents/skills/remix/references/middleware-and-server.md +5 -2
- package/template/.agents/skills/remix/references/mixins-styling-events.md +1 -1
- package/template/.agents/skills/remix/references/routing-and-controllers.md +1 -1
- package/template/.agents/skills/remix/references/testing-patterns.md +1 -1
- package/template/AGENTS.md +2 -3
- package/template/README.md +2 -3
- package/template/app/actions/controller.tsx +2 -4
- package/template/app/actions/document.tsx +7 -4
- package/template/app/actions/public/entry.ts +15 -29
- package/template/app/assets.ts +7 -8
- package/template/app/router.ts +5 -3
- package/template/app/middleware/render.tsx +0 -78
|
@@ -66,31 +66,17 @@ export const Counter = clientEntry(
|
|
|
66
66
|
)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
On the server,
|
|
69
|
+
On the server, pass the asset server to the standard render middleware so source file URLs become browser-loadable asset URLs without hard-coding deployment paths in component modules:
|
|
70
70
|
|
|
71
71
|
```tsx
|
|
72
|
-
|
|
73
|
-
async resolveClientEntry(entryId, component) {
|
|
74
|
-
let exportName = entryId.split('#')[1] || component.name
|
|
75
|
-
if (!exportName) {
|
|
76
|
-
throw new Error(`Unable to resolve client entry export for ${entryId}`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let [href, preloads] = await Promise.all([
|
|
80
|
-
assetServer.getHref(entryId),
|
|
81
|
-
assetServer.getPreloads(entryId),
|
|
82
|
-
])
|
|
72
|
+
import { render } from 'remix/middleware/render'
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
exportName,
|
|
87
|
-
preloads,
|
|
88
|
-
}
|
|
89
|
-
},
|
|
74
|
+
let router = createRouter({
|
|
75
|
+
middleware: [render({ assets: assetServer })],
|
|
90
76
|
})
|
|
91
77
|
```
|
|
92
78
|
|
|
93
|
-
If the module export name differs from the component function name, include `#ExportName` in the entry ID
|
|
79
|
+
If the module export name differs from the component function name, include `#ExportName` in the entry ID. Custom rendering pipelines may instead provide the exact export name through `renderToStream({ resolveClientEntry })`.
|
|
94
80
|
|
|
95
81
|
On the server, `clientEntry` components render like any other component. The server wraps their output in comment markers and serializes props into a `<script type="application/json">` tag.
|
|
96
82
|
|
|
@@ -102,39 +88,51 @@ The resolved `preloads` array contains browser module hrefs. During server rende
|
|
|
102
88
|
|
|
103
89
|
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
|
|
104
90
|
|
|
91
|
+
Client entries introduced by later frame responses may depend on import map entries that were not in the initial document. Browsers without native support for multiple import maps cannot resolve those modules with `import()`. Apps that use asset server import maps and target these browsers can opt into `remix/multiple-import-maps-polyfill`:
|
|
92
|
+
|
|
105
93
|
```tsx
|
|
106
|
-
import
|
|
94
|
+
import {
|
|
95
|
+
detectMultipleImportMapSupport,
|
|
96
|
+
importModule,
|
|
97
|
+
preloadShim,
|
|
98
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
107
99
|
import { run } from 'remix/ui'
|
|
108
100
|
|
|
109
101
|
const app = run({
|
|
110
102
|
async loadModule(moduleUrl, exportName) {
|
|
111
|
-
let mod = await
|
|
112
|
-
|
|
103
|
+
let mod = await importModule(moduleUrl)
|
|
104
|
+
let Component = mod[exportName]
|
|
105
|
+
if (typeof Component !== 'function') {
|
|
106
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
107
|
+
}
|
|
108
|
+
return Component
|
|
113
109
|
},
|
|
114
|
-
async
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
headers,
|
|
120
|
-
method: options?.method,
|
|
121
|
-
signal: options?.signal,
|
|
122
|
-
})
|
|
123
|
-
return response.body ?? (await response.text())
|
|
110
|
+
async processClientEntryPreloads(preloads) {
|
|
111
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
112
|
+
|
|
113
|
+
preloadShim(preloads)
|
|
114
|
+
return []
|
|
124
115
|
},
|
|
125
116
|
})
|
|
126
117
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (options.encType !== 'application/x-www-form-urlencoded') return formData
|
|
118
|
+
app.addEventListener('error', (event) => {
|
|
119
|
+
console.error('Component error:', event.error)
|
|
120
|
+
})
|
|
131
121
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
await app.ready()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The support check keeps native imports and modulepreload links in browsers that support multiple import maps. Other browsers use the polyfill for late client entries and their preloads. Apps that do not need this compatibility can use `import()` directly in `loadModule` and omit `processClientEntryPreloads`.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { run } from 'remix/ui'
|
|
129
|
+
|
|
130
|
+
const app = run({
|
|
131
|
+
async loadModule(moduleUrl, exportName) {
|
|
132
|
+
let mod = await import(moduleUrl)
|
|
133
|
+
return mod[exportName]
|
|
134
|
+
},
|
|
135
|
+
})
|
|
138
136
|
|
|
139
137
|
app.addEventListener('error', (event) => {
|
|
140
138
|
console.error('Component error:', event.error)
|
|
@@ -143,10 +141,22 @@ app.addEventListener('error', (event) => {
|
|
|
143
141
|
await app.ready()
|
|
144
142
|
```
|
|
145
143
|
|
|
144
|
+
By default, `run()` resolves frames with `fetch()`, requests `text/html`, and forwards the submitted
|
|
145
|
+
method and abort signal. GET form values are already encoded in `src`; non-GET submissions use
|
|
146
|
+
`URLSearchParams` for `application/x-www-form-urlencoded`, CRLF-delimited text for `text/plain`, and
|
|
147
|
+
`FormData` for `multipart/form-data`. Provide `resolveFrame` when an app needs additional headers,
|
|
148
|
+
another body encoding, or a different response policy.
|
|
149
|
+
|
|
150
|
+
Add `data-rmx-document` to a link or form to leave its navigation to the browser.
|
|
151
|
+
|
|
152
|
+
The default resolver rejects non-OK responses with an error containing their status and status text.
|
|
153
|
+
A custom `resolveFrame` may return a `Response` with any status when it wants the runtime to render
|
|
154
|
+
the response body.
|
|
155
|
+
|
|
146
156
|
### `run` options
|
|
147
157
|
|
|
148
158
|
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
|
|
149
|
-
- **`resolveFrame(src, options)`** (optional) —
|
|
159
|
+
- **`resolveFrame(src, options)`** (optional) — overrides the default `fetch()` resolver 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`.
|
|
150
160
|
|
|
151
161
|
### `app` methods
|
|
152
162
|
|
|
@@ -217,17 +227,17 @@ When a frame reloads, matching DOM nodes are updated in place. Client entries re
|
|
|
217
227
|
|
|
218
228
|
### Form navigation
|
|
219
229
|
|
|
220
|
-
When `run(
|
|
230
|
+
When `run()` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
|
|
221
231
|
|
|
222
232
|
- Forms target `handle.frames.top` by default.
|
|
223
|
-
- `rmx-target` selects a named frame.
|
|
224
|
-
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
|
|
225
|
-
- `rmx-history="push|replace"` overrides how the navigation updates history.
|
|
226
|
-
- `rmx-reset-scroll="false"` preserves scroll position.
|
|
227
|
-
- `rmx-document` opts back into a document submission.
|
|
233
|
+
- `data-rmx-target` selects a named frame.
|
|
234
|
+
- `data-rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
|
|
235
|
+
- `data-rmx-history="push|replace"` overrides how the navigation updates history.
|
|
236
|
+
- `data-rmx-reset-scroll="false"` preserves scroll position.
|
|
237
|
+
- `data-rmx-document` opts back into a document submission.
|
|
228
238
|
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
|
|
229
239
|
|
|
230
|
-
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.
|
|
240
|
+
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 `data-rmx-history` attribute overrides those defaults.
|
|
231
241
|
|
|
232
242
|
### Nested frames
|
|
233
243
|
|
|
@@ -235,9 +245,22 @@ Frames can nest. Each frame owns its own DOM region and hydrates client entries
|
|
|
235
245
|
|
|
236
246
|
## Server Rendering
|
|
237
247
|
|
|
248
|
+
Normal applications install the conventional middleware and render at the action boundary:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
import { render } from 'remix/middleware/render'
|
|
252
|
+
import { createRouter } from 'remix/router'
|
|
253
|
+
|
|
254
|
+
let router = createRouter({ middleware: [render()] })
|
|
255
|
+
|
|
256
|
+
router.get('/', (context) => context.render(<App />, { status: 200 }))
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The middleware seeds frame URLs from the request, resolves nested and targeted frames through the current router, forwards session and authentication headers safely, follows frame redirects, preserves application error bodies, and cancels rendering with the request.
|
|
260
|
+
|
|
238
261
|
### `renderToStream`
|
|
239
262
|
|
|
240
|
-
|
|
263
|
+
Use this low-level API when replacing the standard response pipeline. It renders a component tree to a `ReadableStream<Uint8Array>`, sends initial HTML immediately, and streams frame content as it resolves:
|
|
241
264
|
|
|
242
265
|
```tsx
|
|
243
266
|
import { renderToStream } from 'remix/ui/server'
|
|
@@ -292,7 +315,7 @@ navigate('/dashboard', { history: 'replace' })
|
|
|
292
315
|
|
|
293
316
|
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
|
|
294
317
|
|
|
295
|
-
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
|
|
318
|
+
Attributes understood by the runtime: `data-rmx-target`, `data-rmx-src`, `data-rmx-history`, `data-rmx-reset-scroll`, `data-rmx-document`.
|
|
296
319
|
|
|
297
320
|
## Head Management
|
|
298
321
|
|
|
@@ -24,6 +24,7 @@ import { compression } from 'remix/middleware/compression'
|
|
|
24
24
|
import { formData } from 'remix/middleware/form-data'
|
|
25
25
|
import { logger } from 'remix/middleware/logger'
|
|
26
26
|
import { methodOverride } from 'remix/middleware/method-override'
|
|
27
|
+
import { render } from 'remix/middleware/render'
|
|
27
28
|
import { session } from 'remix/middleware/session'
|
|
28
29
|
import { staticFiles } from 'remix/middleware/static'
|
|
29
30
|
import { asyncContext } from 'remix/middleware/async-context'
|
|
@@ -42,6 +43,7 @@ middleware.push(session(cookie, storage))
|
|
|
42
43
|
middleware.push(asyncContext())
|
|
43
44
|
middleware.push(loadDatabase())
|
|
44
45
|
middleware.push(loadAuth())
|
|
46
|
+
middleware.push(render({ assets }))
|
|
45
47
|
|
|
46
48
|
let router = createRouter({ middleware })
|
|
47
49
|
```
|
|
@@ -62,11 +64,12 @@ let router = createRouter({ middleware })
|
|
|
62
64
|
| `asyncContext()` | `remix/middleware/async-context` | Helpers outside handlers need request context via `getContext()` | Add before helpers rely on it |
|
|
63
65
|
| `auth({ schemes })` | `remix/middleware/auth` | Resolve auth state into `context.get(Auth)` | Run after `session()` for session-backed auth |
|
|
64
66
|
| `requireAuth()` | `remix/middleware/auth` | A controller or action must reject anonymous access | Usually controller middleware or action middleware |
|
|
67
|
+
| `render({ assets? })` | `remix/middleware/render` | Actions render Remix UI through `context.render(node, init)` | Pass the asset server for source-based client entries |
|
|
65
68
|
|
|
66
69
|
### Static files vs browser modules
|
|
67
70
|
|
|
68
71
|
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
|
|
69
|
-
- Use `remix/assets` when browser modules should be compiled and served from source files with
|
|
72
|
+
- Use `remix/assets` when browser modules should be compiled and served from source files with dependency resolution, preloads, or fingerprinted URLs
|
|
70
73
|
- `public/` directories inside `app/` hold browser-reachable source for the asset server
|
|
71
74
|
|
|
72
75
|
### Ordering notes
|
|
@@ -79,7 +82,7 @@ let router = createRouter({ middleware })
|
|
|
79
82
|
|
|
80
83
|
### Common stacks
|
|
81
84
|
|
|
82
|
-
- **Session-backed HTML app** -> `compression()`, `staticFiles()`, optional `cop()`, `formData()`, `methodOverride()`, `session()`, optional `csrf()`, `asyncContext()`, `auth({ schemes })`
|
|
85
|
+
- **Session-backed HTML app** -> `compression()`, `staticFiles()`, optional `cop()`, `formData()`, `methodOverride()`, `session()`, optional `csrf()`, `asyncContext()`, `auth({ schemes })`, `render({ assets })`
|
|
83
86
|
- **Cross-origin API** -> `compression()`, `cors()`, optional `asyncContext()`, optional `auth({ schemes })`
|
|
84
87
|
- **Upload flow** -> `compression()`, `staticFiles()`, `formData({ uploadHandler })`, then sessions, auth, and data-loading middleware as needed
|
|
85
88
|
- **Optional development HMR** -> keep `server.ts` as the child app server, add `hmr.ts` for `remix/node-hmr`, and proxy public requests through `createHmrReadyFetch()`
|
|
@@ -115,7 +115,7 @@ 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.
|
|
118
|
+
On a native anchor, the `history` option renders the corresponding `data-rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
|
|
119
119
|
|
|
120
120
|
## Native press and keyboard interactions
|
|
121
121
|
|
|
@@ -252,7 +252,7 @@ export const routes = route({
|
|
|
252
252
|
export default createController(routes, {
|
|
253
253
|
actions: {
|
|
254
254
|
async assets({ request }) {
|
|
255
|
-
return (await
|
|
255
|
+
return (await assets.fetch(request)) ?? new Response('Not Found', { status: 404 })
|
|
256
256
|
},
|
|
257
257
|
home() {
|
|
258
258
|
return render(<HomePage />)
|
|
@@ -74,7 +74,7 @@ Configure discovery and coverage in the `test` section of `remix.json` or with C
|
|
|
74
74
|
|
|
75
75
|
```jsonc
|
|
76
76
|
{
|
|
77
|
-
"$schema": "
|
|
77
|
+
"$schema": "./node_modules/remix/schema/remix.json",
|
|
78
78
|
"test": {
|
|
79
79
|
"files": ["**/*.test{,.e2e}.{ts,tsx}"],
|
|
80
80
|
"e2eFiles": ["**/*.test.e2e.{ts,tsx}"],
|
package/template/AGENTS.md
CHANGED
|
@@ -23,9 +23,8 @@ Refer to ./.agents/skills/remix/SKILL.md
|
|
|
23
23
|
- `app/actions/home-page.tsx` and `app/actions/document.tsx` render the route-owned starter UI
|
|
24
24
|
- `app/actions/public/` contains the browser runtime entry and interactive prompt button
|
|
25
25
|
- `app/routes.ts` defines the shared route contract used by server and browser modules for type-safe hrefs
|
|
26
|
-
- `app/router.ts` wires routes to route handlers
|
|
27
|
-
- `app/
|
|
28
|
-
- `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer
|
|
26
|
+
- `app/router.ts` wires routes to route handlers and installs the standard Remix UI renderer used by actions
|
|
27
|
+
- `app/assets.ts` owns the server-side asset pipeline used by the asset route and render middleware
|
|
29
28
|
- Root `public/` contains static files served unchanged from the app root
|
|
30
29
|
|
|
31
30
|
## Route Ownership
|
package/template/README.md
CHANGED
|
@@ -8,9 +8,8 @@ A minimal Remix application starter with a home page.
|
|
|
8
8
|
- `app/actions/home-page.tsx` and `app/actions/document.tsx` render the route-owned starter UI.
|
|
9
9
|
- `app/actions/public/` contains the browser runtime entry and interactive prompt button.
|
|
10
10
|
- `app/routes.ts` defines the shared route contract used by server and browser modules for type-safe hrefs.
|
|
11
|
-
- `app/router.ts` wires routes to handlers.
|
|
12
|
-
- `app/
|
|
13
|
-
- `app/assets.ts` owns the server-side asset pipeline used by the asset route and renderer.
|
|
11
|
+
- `app/router.ts` wires routes to handlers and installs the standard Remix UI renderer used by actions.
|
|
12
|
+
- `app/assets.ts` owns the server-side asset pipeline used by the asset route and render middleware.
|
|
14
13
|
- Root `public/` contains static files served unchanged from the app root.
|
|
15
14
|
|
|
16
15
|
## Growing The App
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { createController } from 'remix/router'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { assets } from '../assets.ts'
|
|
4
4
|
import { routes } from '../routes.ts'
|
|
5
5
|
import { HomePage } from './home-page.tsx'
|
|
6
6
|
|
|
7
7
|
export default createController(routes, {
|
|
8
8
|
actions: {
|
|
9
9
|
async assets(context) {
|
|
10
|
-
return (
|
|
11
|
-
(await assetServer.fetch(context.request)) ?? new Response('Not Found', { status: 404 })
|
|
12
|
-
)
|
|
10
|
+
return (await assets.fetch(context.request)) ?? new Response('Not Found', { status: 404 })
|
|
13
11
|
},
|
|
14
12
|
home(context) {
|
|
15
13
|
return context.render(<HomePage />)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Handle, RemixNode } from 'remix/ui'
|
|
2
2
|
import { css } from 'remix/ui'
|
|
3
|
+
import { ImportMap } from 'remix/ui/server'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { scriptEntry } from '../assets.ts'
|
|
5
6
|
|
|
6
7
|
export interface DocumentProps {
|
|
7
8
|
children?: RemixNode
|
|
@@ -14,6 +15,7 @@ const DEFAULT_TITLE = readAppDisplayName('%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%
|
|
|
14
15
|
export function Document(handle: Handle<DocumentProps>) {
|
|
15
16
|
return () => {
|
|
16
17
|
let { children, head, title = DEFAULT_TITLE } = handle.props
|
|
18
|
+
let { href, importMap, preloads } = scriptEntry
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
21
|
<html lang="en">
|
|
@@ -24,10 +26,11 @@ export function Document(handle: Handle<DocumentProps>) {
|
|
|
24
26
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
25
27
|
<title>{title}</title>
|
|
26
28
|
{head}
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
+
<ImportMap value={importMap} />
|
|
30
|
+
{preloads.map((preloadHref) => (
|
|
31
|
+
<link key={preloadHref} rel="modulepreload" href={preloadHref} />
|
|
29
32
|
))}
|
|
30
|
-
<script type="module" src={
|
|
33
|
+
<script type="module" src={href}></script>
|
|
31
34
|
</head>
|
|
32
35
|
<body mix={css({ margin: 0 })}>{children}</body>
|
|
33
36
|
</html>
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectMultipleImportMapSupport,
|
|
3
|
+
importModule,
|
|
4
|
+
preloadShim,
|
|
5
|
+
} from 'remix/multiple-import-maps-polyfill'
|
|
1
6
|
import { run } from 'remix/ui'
|
|
2
7
|
|
|
3
8
|
const app = run({
|
|
4
9
|
async loadModule(moduleUrl, exportName) {
|
|
5
|
-
let mod = await
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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>`
|
|
10
|
+
let mod = await importModule(moduleUrl)
|
|
11
|
+
let Component = mod[exportName]
|
|
12
|
+
if (typeof Component !== 'function') {
|
|
13
|
+
throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
|
|
17
14
|
}
|
|
15
|
+
return Component
|
|
16
|
+
},
|
|
17
|
+
async processClientEntryPreloads(preloads) {
|
|
18
|
+
if (await detectMultipleImportMapSupport()) return preloads
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
return
|
|
20
|
+
preloadShim(preloads)
|
|
21
|
+
return []
|
|
21
22
|
},
|
|
22
23
|
})
|
|
23
24
|
|
|
@@ -31,18 +32,3 @@ if (import.meta.hot) {
|
|
|
31
32
|
}
|
|
32
33
|
})
|
|
33
34
|
}
|
|
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
|
-
}
|
package/template/app/assets.ts
CHANGED
|
@@ -6,13 +6,10 @@ const nodeEnv = process.env.NODE_ENV ?? 'development'
|
|
|
6
6
|
const isDevelopment = nodeEnv === 'development'
|
|
7
7
|
const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const assets = createAssetServer({
|
|
10
10
|
basePath: '/assets',
|
|
11
11
|
rootDir,
|
|
12
|
-
|
|
13
|
-
'app/*path': 'app/*path',
|
|
14
|
-
'node_modules/*path': 'node_modules/*path',
|
|
15
|
-
},
|
|
12
|
+
|
|
16
13
|
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
17
14
|
allowPackages: ['remix'],
|
|
18
15
|
denyFiles: ['app/**/*.test.*'],
|
|
@@ -20,12 +17,14 @@ export const assetServer = createAssetServer({
|
|
|
20
17
|
minify: !isDevelopment,
|
|
21
18
|
watch: isDevelopment,
|
|
22
19
|
hmr: isHmr
|
|
23
|
-
?
|
|
20
|
+
? {
|
|
21
|
+
channel: async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(),
|
|
22
|
+
moduleImporter: 'remix/multiple-import-maps-polyfill',
|
|
23
|
+
}
|
|
24
24
|
: undefined,
|
|
25
25
|
scripts: { loaders: isHmr ? [uiHmr()] : undefined },
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
const entry = 'app/actions/public/entry.ts'
|
|
29
29
|
|
|
30
|
-
export const
|
|
31
|
-
export const entryPreloads = await assetServer.getPreloads(entry)
|
|
30
|
+
export const scriptEntry = await assets.getScriptEntry(entry)
|
package/template/app/router.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createRouter, type MiddlewareContext } from 'remix/router'
|
|
2
|
+
import { render } from 'remix/middleware/render'
|
|
2
3
|
import { staticFiles } from 'remix/middleware/static'
|
|
3
4
|
|
|
4
5
|
import controller from './actions/controller.tsx'
|
|
5
|
-
import {
|
|
6
|
+
import { assets } from './assets.ts'
|
|
6
7
|
import { routes } from './routes.ts'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const renderMiddleware = render({ assets })
|
|
10
|
+
type AppContext = MiddlewareContext<[typeof renderMiddleware]>
|
|
9
11
|
|
|
10
12
|
declare module 'remix/router' {
|
|
11
13
|
interface RouterTypes {
|
|
@@ -14,7 +16,7 @@ declare module 'remix/router' {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export const router = createRouter<AppContext>({
|
|
17
|
-
middleware: [staticFiles('./public', { index: false }),
|
|
19
|
+
middleware: [staticFiles('./public', { index: false }), renderMiddleware],
|
|
18
20
|
})
|
|
19
21
|
|
|
20
22
|
router.map(routes, controller)
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import type { Router } from 'remix/router'
|
|
4
|
-
import { renderWith } from 'remix/middleware/render'
|
|
5
|
-
import { createHtmlResponse } from 'remix/response/html'
|
|
6
|
-
import type { RemixNode } from 'remix/ui'
|
|
7
|
-
import { renderToStream } from 'remix/ui/server'
|
|
8
|
-
|
|
9
|
-
import { assetServer } from '../assets.ts'
|
|
10
|
-
|
|
11
|
-
export function render() {
|
|
12
|
-
return renderWith(
|
|
13
|
-
({ request, router }) =>
|
|
14
|
-
function render(node: RemixNode, init?: ResponseInit) {
|
|
15
|
-
let stream = renderToStream(node, {
|
|
16
|
-
frameSrc: request.url,
|
|
17
|
-
signal: request.signal,
|
|
18
|
-
resolveFrame: (src) => resolveFrame(router, request, src),
|
|
19
|
-
// Server rendering turns client entries into browser module URLs and preloads.
|
|
20
|
-
async resolveClientEntry(entryId, component) {
|
|
21
|
-
if (!entryId.startsWith('file://')) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Expected \`import.meta.url\` for clientEntry ID, received '${entryId}'`,
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let [href, preloads] = await Promise.all([
|
|
28
|
-
assetServer.getHref(entryId),
|
|
29
|
-
assetServer.getPreloads(entryId),
|
|
30
|
-
])
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
href,
|
|
34
|
-
exportName: entryId.split('#')[1] || component.name || titleCaseFileName(entryId),
|
|
35
|
-
preloads,
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return createHtmlResponse(stream, init)
|
|
41
|
-
},
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function resolveFrame(router: Router, request: Request, src: string) {
|
|
46
|
-
let url = new URL(src, request.url)
|
|
47
|
-
|
|
48
|
-
let headers = new Headers()
|
|
49
|
-
headers.set('Accept', 'text/html')
|
|
50
|
-
|
|
51
|
-
let cookie = request.headers.get('Cookie')
|
|
52
|
-
if (cookie) headers.set('Cookie', cookie)
|
|
53
|
-
|
|
54
|
-
let response = await router.fetch(
|
|
55
|
-
new Request(url, {
|
|
56
|
-
method: 'GET',
|
|
57
|
-
headers,
|
|
58
|
-
signal: request.signal,
|
|
59
|
-
}),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (response.body) return response.body
|
|
67
|
-
return await response.text()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function titleCaseFileName(fileUrl: string): string {
|
|
71
|
-
let url = new URL(fileUrl)
|
|
72
|
-
let fileName = path.basename(url.pathname, path.extname(url.pathname))
|
|
73
|
-
return fileName
|
|
74
|
-
.split(/[^A-Za-z0-9]+/)
|
|
75
|
-
.filter(Boolean)
|
|
76
|
-
.map((segment) => segment[0]!.toUpperCase() + segment.slice(1))
|
|
77
|
-
.join('')
|
|
78
|
-
}
|