@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.
Files changed (55) hide show
  1. package/README.md +52 -3
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/lib/cli-context.d.ts.map +1 -1
  6. package/dist/lib/cli-context.js +5 -2
  7. package/dist/lib/cli.d.ts +1 -1
  8. package/dist/lib/cli.js +5 -1
  9. package/dist/lib/commands/assets.d.ts +4 -0
  10. package/dist/lib/commands/assets.d.ts.map +1 -0
  11. package/dist/lib/commands/assets.js +90 -0
  12. package/dist/lib/commands/db.d.ts.map +1 -1
  13. package/dist/lib/commands/db.js +63 -5
  14. package/dist/lib/commands/help.d.ts.map +1 -1
  15. package/dist/lib/commands/help.js +7 -0
  16. package/dist/lib/completion.d.ts.map +1 -1
  17. package/dist/lib/completion.js +25 -1
  18. package/dist/lib/database-command.d.ts +5 -1
  19. package/dist/lib/database-command.d.ts.map +1 -1
  20. package/dist/lib/database-command.js +1 -0
  21. package/dist/lib/errors.d.ts +6 -0
  22. package/dist/lib/errors.d.ts.map +1 -1
  23. package/dist/lib/errors.js +10 -0
  24. package/dist/lib/remix-config.d.ts +22 -0
  25. package/dist/lib/remix-config.d.ts.map +1 -1
  26. package/dist/lib/remix-config.js +80 -4
  27. package/package.json +6 -5
  28. package/schema/remix.json +49 -1
  29. package/src/index.ts +10 -0
  30. package/src/lib/cli-context.ts +10 -2
  31. package/src/lib/cli.ts +6 -1
  32. package/src/lib/commands/assets.ts +109 -0
  33. package/src/lib/commands/db.ts +75 -5
  34. package/src/lib/commands/help.ts +8 -0
  35. package/src/lib/completion.ts +41 -1
  36. package/src/lib/database-command.ts +6 -1
  37. package/src/lib/errors.ts +11 -0
  38. package/src/lib/remix-config.ts +128 -4
  39. package/template/.agents/skills/remix/SKILL.md +7 -5
  40. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +22 -20
  41. package/template/.agents/skills/remix/references/auth-and-sessions.md +1 -15
  42. package/template/.agents/skills/remix/references/component-model.md +18 -16
  43. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +75 -52
  44. package/template/.agents/skills/remix/references/middleware-and-server.md +5 -2
  45. package/template/.agents/skills/remix/references/mixins-styling-events.md +1 -1
  46. package/template/.agents/skills/remix/references/routing-and-controllers.md +1 -1
  47. package/template/.agents/skills/remix/references/testing-patterns.md +1 -1
  48. package/template/AGENTS.md +2 -3
  49. package/template/README.md +2 -3
  50. package/template/app/actions/controller.tsx +2 -4
  51. package/template/app/actions/document.tsx +7 -4
  52. package/template/app/actions/public/entry.ts +15 -29
  53. package/template/app/assets.ts +7 -8
  54. package/template/app/router.ts +5 -3
  55. 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, provide `resolveClientEntry` to `renderToStream(...)` so source file URLs become browser-loadable asset URLs. Keep this resolution in the render helper so component modules do not hard-code deployment-specific asset paths:
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
- let stream = renderToStream(<App />, {
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
- return {
85
- href,
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 or return the exact export name from `resolveClientEntry`. A render helper that only supports source-owned entries can also fail fast when `entryId` is not a `file://` URL.
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 type { ResolveFrameOptions } from 'remix/ui'
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 import(moduleUrl)
112
- return mod[exportName]
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 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())
110
+ async processClientEntryPreloads(preloads) {
111
+ if (await detectMultipleImportMapSupport()) return preloads
112
+
113
+ preloadShim(preloads)
114
+ return []
124
115
  },
125
116
  })
126
117
 
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
118
+ app.addEventListener('error', (event) => {
119
+ console.error('Component error:', event.error)
120
+ })
131
121
 
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
- }
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) — called 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`.
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({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
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
- Renders a component tree to a `ReadableStream<Uint8Array>`. Sends initial HTML immediately and streams frame content as it resolves:
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 import rewriting, preloads, or fingerprinted URLs
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 assetServer.fetch(request)) ?? new Response('Not Found', { status: 404 })
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": "https://remix.run/schemas/remix.json",
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}"],
@@ -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/middleware/render.tsx` installs the request-scoped renderer used by actions
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
@@ -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/middleware/render.tsx` installs the request-scoped renderer used by actions.
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 { assetServer } from '../assets.ts'
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 { entryHref, entryPreloads } from '../assets.ts'
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
- {entryPreloads.map((href) => (
28
- <link key={href} rel="modulepreload" href={href} />
29
+ <ImportMap value={importMap} />
30
+ {preloads.map((preloadHref) => (
31
+ <link key={preloadHref} rel="modulepreload" href={preloadHref} />
29
32
  ))}
30
- <script type="module" src={entryHref}></script>
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 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>`
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
- if (response.body) return response.body
20
- return await response.text()
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
- }
@@ -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 assetServer = createAssetServer({
9
+ export const assets = createAssetServer({
10
10
  basePath: '/assets',
11
11
  rootDir,
12
- fileMap: {
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
- ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
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 entryHref = await assetServer.getHref(entry)
31
- export const entryPreloads = await assetServer.getPreloads(entry)
30
+ export const scriptEntry = await assets.getScriptEntry(entry)
@@ -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 { render } from './middleware/render.tsx'
6
+ import { assets } from './assets.ts'
6
7
  import { routes } from './routes.ts'
7
8
 
8
- type AppContext = MiddlewareContext<[ReturnType<typeof render>]>
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 }), render()],
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
- }