@remix-run/cli 0.4.0 → 0.6.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 +8 -7
- 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 +8 -13
- 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 +43 -48
- package/template/.agents/skills/remix/references/middleware-and-server.md +4 -1
- 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/assets.ts +4 -7
- package/template/app/router.ts +5 -3
- package/template/app/middleware/render.tsx +0 -72
|
@@ -66,36 +66,29 @@ 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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error(`Unable to resolve client entry export for ${entryId}`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
href: await assetServer.getHref(entryId),
|
|
81
|
-
exportName,
|
|
82
|
-
}
|
|
83
|
-
},
|
|
72
|
+
import { render } from 'remix/middleware/render'
|
|
73
|
+
|
|
74
|
+
let router = createRouter({
|
|
75
|
+
middleware: [render({ assets: assetServer })],
|
|
84
76
|
})
|
|
85
77
|
```
|
|
86
78
|
|
|
87
|
-
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 })`.
|
|
88
80
|
|
|
89
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.
|
|
90
82
|
|
|
91
83
|
Client entry props must be serializable: strings, numbers, booleans, `null`, `undefined`, plain objects/arrays of the above, JSX elements, and `<Frame>` elements. Functions and class instances cannot be passed.
|
|
92
84
|
|
|
85
|
+
The resolved `preloads` array contains browser module hrefs. During server rendering these are emitted as `<link rel="modulepreload">` tags, including preloads discovered in blocking frames. When a later frame response introduces a client entry, its preloads start before the entry module is loaded.
|
|
86
|
+
|
|
93
87
|
## Booting the Client
|
|
94
88
|
|
|
95
89
|
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
|
|
96
90
|
|
|
97
91
|
```tsx
|
|
98
|
-
import type { ResolveFrameOptions } from 'remix/ui'
|
|
99
92
|
import { run } from 'remix/ui'
|
|
100
93
|
|
|
101
94
|
const app = run({
|
|
@@ -103,31 +96,8 @@ const app = run({
|
|
|
103
96
|
let mod = await import(moduleUrl)
|
|
104
97
|
return mod[exportName]
|
|
105
98
|
},
|
|
106
|
-
async resolveFrame(src, options) {
|
|
107
|
-
let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
|
|
108
|
-
if (options?.target) headers.set('x-remix-target', options.target)
|
|
109
|
-
let response = await fetch(src, {
|
|
110
|
-
body: getRequestBody(options),
|
|
111
|
-
headers,
|
|
112
|
-
method: options?.method,
|
|
113
|
-
signal: options?.signal,
|
|
114
|
-
})
|
|
115
|
-
return response.body ?? (await response.text())
|
|
116
|
-
},
|
|
117
99
|
})
|
|
118
100
|
|
|
119
|
-
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
|
|
120
|
-
let formData = options?.formData
|
|
121
|
-
if (!formData) return
|
|
122
|
-
if (options.encType !== 'application/x-www-form-urlencoded') return formData
|
|
123
|
-
|
|
124
|
-
let body = new URLSearchParams()
|
|
125
|
-
for (let [name, value] of formData) {
|
|
126
|
-
body.append(name, typeof value === 'string' ? value : value.name)
|
|
127
|
-
}
|
|
128
|
-
return body
|
|
129
|
-
}
|
|
130
|
-
|
|
131
101
|
app.addEventListener('error', (event) => {
|
|
132
102
|
console.error('Component error:', event.error)
|
|
133
103
|
})
|
|
@@ -135,10 +105,22 @@ app.addEventListener('error', (event) => {
|
|
|
135
105
|
await app.ready()
|
|
136
106
|
```
|
|
137
107
|
|
|
108
|
+
By default, `run()` resolves frames with `fetch()`, requests `text/html`, and forwards the submitted
|
|
109
|
+
method and abort signal. GET form values are already encoded in `src`; non-GET submissions use
|
|
110
|
+
`URLSearchParams` for `application/x-www-form-urlencoded`, CRLF-delimited text for `text/plain`, and
|
|
111
|
+
`FormData` for `multipart/form-data`. Provide `resolveFrame` when an app needs additional headers,
|
|
112
|
+
another body encoding, or a different response policy.
|
|
113
|
+
|
|
114
|
+
Add `data-rmx-document` to a link or form to leave its navigation to the browser.
|
|
115
|
+
|
|
116
|
+
The default resolver rejects non-OK responses with an error containing their status and status text.
|
|
117
|
+
A custom `resolveFrame` may return a `Response` with any status when it wants the runtime to render
|
|
118
|
+
the response body.
|
|
119
|
+
|
|
138
120
|
### `run` options
|
|
139
121
|
|
|
140
122
|
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
|
|
141
|
-
- **`resolveFrame(src, options)`** (optional) —
|
|
123
|
+
- **`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`.
|
|
142
124
|
|
|
143
125
|
### `app` methods
|
|
144
126
|
|
|
@@ -209,17 +191,17 @@ When a frame reloads, matching DOM nodes are updated in place. Client entries re
|
|
|
209
191
|
|
|
210
192
|
### Form navigation
|
|
211
193
|
|
|
212
|
-
When `run(
|
|
194
|
+
When `run()` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
|
|
213
195
|
|
|
214
196
|
- Forms target `handle.frames.top` by default.
|
|
215
|
-
- `rmx-target` selects a named frame.
|
|
216
|
-
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
|
|
217
|
-
- `rmx-history="push|replace"` overrides how the navigation updates history.
|
|
218
|
-
- `rmx-reset-scroll="false"` preserves scroll position.
|
|
219
|
-
- `rmx-document` opts back into a document submission.
|
|
197
|
+
- `data-rmx-target` selects a named frame.
|
|
198
|
+
- `data-rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
|
|
199
|
+
- `data-rmx-history="push|replace"` overrides how the navigation updates history.
|
|
200
|
+
- `data-rmx-reset-scroll="false"` preserves scroll position.
|
|
201
|
+
- `data-rmx-document` opts back into a document submission.
|
|
220
202
|
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
|
|
221
203
|
|
|
222
|
-
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
|
|
204
|
+
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.
|
|
223
205
|
|
|
224
206
|
### Nested frames
|
|
225
207
|
|
|
@@ -227,9 +209,22 @@ Frames can nest. Each frame owns its own DOM region and hydrates client entries
|
|
|
227
209
|
|
|
228
210
|
## Server Rendering
|
|
229
211
|
|
|
212
|
+
Normal applications install the conventional middleware and render at the action boundary:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { render } from 'remix/middleware/render'
|
|
216
|
+
import { createRouter } from 'remix/router'
|
|
217
|
+
|
|
218
|
+
let router = createRouter({ middleware: [render()] })
|
|
219
|
+
|
|
220
|
+
router.get('/', (context) => context.render(<App />, { status: 200 }))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
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.
|
|
224
|
+
|
|
230
225
|
### `renderToStream`
|
|
231
226
|
|
|
232
|
-
|
|
227
|
+
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:
|
|
233
228
|
|
|
234
229
|
```tsx
|
|
235
230
|
import { renderToStream } from 'remix/ui/server'
|
|
@@ -284,7 +279,7 @@ navigate('/dashboard', { history: 'replace' })
|
|
|
284
279
|
|
|
285
280
|
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
|
|
286
281
|
|
|
287
|
-
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
|
|
282
|
+
Attributes understood by the runtime: `data-rmx-target`, `data-rmx-src`, `data-rmx-history`, `data-rmx-reset-scroll`, `data-rmx-document`.
|
|
288
283
|
|
|
289
284
|
## Head Management
|
|
290
285
|
|
|
@@ -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,6 +64,7 @@ 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
|
|
|
@@ -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 />)
|
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.*'],
|
|
@@ -27,5 +24,5 @@ export const assetServer = createAssetServer({
|
|
|
27
24
|
|
|
28
25
|
const entry = 'app/actions/public/entry.ts'
|
|
29
26
|
|
|
30
|
-
export const entryHref = await
|
|
31
|
-
export const entryPreloads = await
|
|
27
|
+
export const entryHref = await assets.getHref(entry)
|
|
28
|
+
export const entryPreloads = await assets.getPreloads(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,72 +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.
|
|
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
|
-
return {
|
|
28
|
-
href: await assetServer.getHref(entryId),
|
|
29
|
-
exportName: entryId.split('#')[1] || component.name || titleCaseFileName(entryId),
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
return createHtmlResponse(stream, init)
|
|
35
|
-
},
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function resolveFrame(router: Router, request: Request, src: string) {
|
|
40
|
-
let url = new URL(src, request.url)
|
|
41
|
-
|
|
42
|
-
let headers = new Headers()
|
|
43
|
-
headers.set('Accept', 'text/html')
|
|
44
|
-
|
|
45
|
-
let cookie = request.headers.get('Cookie')
|
|
46
|
-
if (cookie) headers.set('Cookie', cookie)
|
|
47
|
-
|
|
48
|
-
let response = await router.fetch(
|
|
49
|
-
new Request(url, {
|
|
50
|
-
method: 'GET',
|
|
51
|
-
headers,
|
|
52
|
-
signal: request.signal,
|
|
53
|
-
}),
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (response.body) return response.body
|
|
61
|
-
return await response.text()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function titleCaseFileName(fileUrl: string): string {
|
|
65
|
-
let url = new URL(fileUrl)
|
|
66
|
-
let fileName = path.basename(url.pathname, path.extname(url.pathname))
|
|
67
|
-
return fileName
|
|
68
|
-
.split(/[^A-Za-z0-9]+/)
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.map((segment) => segment[0]!.toUpperCase() + segment.slice(1))
|
|
71
|
-
.join('')
|
|
72
|
-
}
|