@remix-run/cli 0.5.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 +41 -54
- 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 -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
])
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
href,
|
|
86
|
-
exportName,
|
|
87
|
-
preloads,
|
|
88
|
-
}
|
|
89
|
-
},
|
|
72
|
+
import { render } from 'remix/middleware/render'
|
|
73
|
+
|
|
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
|
|
|
@@ -103,7 +89,6 @@ The resolved `preloads` array contains browser module hrefs. During server rende
|
|
|
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
|
|
|
105
91
|
```tsx
|
|
106
|
-
import type { ResolveFrameOptions } from 'remix/ui'
|
|
107
92
|
import { run } from 'remix/ui'
|
|
108
93
|
|
|
109
94
|
const app = run({
|
|
@@ -111,31 +96,8 @@ const app = run({
|
|
|
111
96
|
let mod = await import(moduleUrl)
|
|
112
97
|
return mod[exportName]
|
|
113
98
|
},
|
|
114
|
-
async resolveFrame(src, options) {
|
|
115
|
-
let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
|
|
116
|
-
if (options?.target) headers.set('x-remix-target', options.target)
|
|
117
|
-
let response = await fetch(src, {
|
|
118
|
-
body: getRequestBody(options),
|
|
119
|
-
headers,
|
|
120
|
-
method: options?.method,
|
|
121
|
-
signal: options?.signal,
|
|
122
|
-
})
|
|
123
|
-
return response.body ?? (await response.text())
|
|
124
|
-
},
|
|
125
99
|
})
|
|
126
100
|
|
|
127
|
-
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
|
|
128
|
-
let formData = options?.formData
|
|
129
|
-
if (!formData) return
|
|
130
|
-
if (options.encType !== 'application/x-www-form-urlencoded') return formData
|
|
131
|
-
|
|
132
|
-
let body = new URLSearchParams()
|
|
133
|
-
for (let [name, value] of formData) {
|
|
134
|
-
body.append(name, typeof value === 'string' ? value : value.name)
|
|
135
|
-
}
|
|
136
|
-
return body
|
|
137
|
-
}
|
|
138
|
-
|
|
139
101
|
app.addEventListener('error', (event) => {
|
|
140
102
|
console.error('Component error:', event.error)
|
|
141
103
|
})
|
|
@@ -143,10 +105,22 @@ app.addEventListener('error', (event) => {
|
|
|
143
105
|
await app.ready()
|
|
144
106
|
```
|
|
145
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
|
+
|
|
146
120
|
### `run` options
|
|
147
121
|
|
|
148
122
|
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
|
|
149
|
-
- **`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`.
|
|
150
124
|
|
|
151
125
|
### `app` methods
|
|
152
126
|
|
|
@@ -217,17 +191,17 @@ When a frame reloads, matching DOM nodes are updated in place. Client entries re
|
|
|
217
191
|
|
|
218
192
|
### Form navigation
|
|
219
193
|
|
|
220
|
-
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.
|
|
221
195
|
|
|
222
196
|
- 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.
|
|
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.
|
|
228
202
|
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
|
|
229
203
|
|
|
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.
|
|
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.
|
|
231
205
|
|
|
232
206
|
### Nested frames
|
|
233
207
|
|
|
@@ -235,9 +209,22 @@ Frames can nest. Each frame owns its own DOM region and hydrates client entries
|
|
|
235
209
|
|
|
236
210
|
## Server Rendering
|
|
237
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
|
+
|
|
238
225
|
### `renderToStream`
|
|
239
226
|
|
|
240
|
-
|
|
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:
|
|
241
228
|
|
|
242
229
|
```tsx
|
|
243
230
|
import { renderToStream } from 'remix/ui/server'
|
|
@@ -292,7 +279,7 @@ navigate('/dashboard', { history: 'replace' })
|
|
|
292
279
|
|
|
293
280
|
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
|
|
294
281
|
|
|
295
|
-
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`.
|
|
296
283
|
|
|
297
284
|
## Head Management
|
|
298
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,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
|
-
}
|