remix 3.0.0-beta.9 → 3.0.0-rc.2

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/src/ui/README.md CHANGED
@@ -79,10 +79,9 @@ function Actions() {
79
79
  }
80
80
  ```
81
81
 
82
- ## Frame Navigation
82
+ ## Client Entry Loading
83
83
 
84
- `run()` progressively enhances same-origin links and forms using a default `resolveFrame` that
85
- fetches the frame source:
84
+ `run()` hydrates client entries by calling `loadModule` for each component module:
86
85
 
87
86
  ```tsx
88
87
  import { run } from 'remix/ui'
@@ -97,6 +96,47 @@ let app = run({
97
96
  await app.ready()
98
97
  ```
99
98
 
99
+ Client entries introduced after the initial document may depend on import maps added at runtime.
100
+ When targeting browsers without native support for multiple import maps, use
101
+ `remix/multiple-import-maps-polyfill` to load these modules and process their preloads:
102
+
103
+ ```tsx
104
+ import {
105
+ detectMultipleImportMapSupport,
106
+ importModule,
107
+ preloadShim,
108
+ } from 'remix/multiple-import-maps-polyfill'
109
+
110
+ let app = run({
111
+ async loadModule(moduleUrl, exportName) {
112
+ let module = await importModule(moduleUrl)
113
+ let Component = module[exportName]
114
+ if (typeof Component !== 'function') {
115
+ throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)
116
+ }
117
+ return Component
118
+ },
119
+ async processClientEntryPreloads(preloads) {
120
+ if (await detectMultipleImportMapSupport()) return preloads
121
+
122
+ preloadShim(preloads)
123
+ return []
124
+ },
125
+ })
126
+ ```
127
+
128
+ ## Frame Navigation
129
+
130
+ The same runtime represents the current document as `app.frames.top` and intercepts eligible
131
+ same-origin links and forms through the browser's Navigation API. Those navigations fetch HTML with
132
+ the frame resolver and update the existing document in place instead of loading a new document.
133
+ This soft-navigation behavior applies even when the page only uses `clientEntry()` and does not
134
+ render an explicit `<Frame>`.
135
+
136
+ Frame navigation requires both `window.navigation` and `NavigateEvent.sourceElement`. Browsers
137
+ missing either capability use document navigation for links, forms, and `navigate()`. Hydration and
138
+ explicit frame reloads still work.
139
+
100
140
  The default resolver is equivalent to:
101
141
 
102
142
  ```js
@@ -108,7 +148,8 @@ async function resolveFrame(src, options) {
108
148
  signal: options?.signal,
109
149
  })
110
150
 
111
- if (!response.ok) {
151
+ let isHtml = response.headers.get('Content-Type')?.toLowerCase().includes('text/html')
152
+ if (response.status >= 500 || (response.status >= 300 && !isHtml)) {
112
153
  throw new Error(`Failed to resolve frame: ${response.status} ${response.statusText}`.trimEnd())
113
154
  }
114
155
 
@@ -149,13 +190,20 @@ CRLF-delimited text, and `multipart/form-data` submissions use `FormData`. Pass
149
190
  `resolveFrame` when the server requires additional headers, another body encoding, or a different
150
191
  response policy.
151
192
 
152
- Add `rmx-document` to a link or form to leave its navigation to the browser.
193
+ Add `data-rmx-document` to a link or form to leave that navigation to the browser. To keep all links
194
+ and forms as document navigations while still hydrating client entries and using explicit frames,
195
+ register a listener before calling `run()`:
196
+
197
+ ```ts
198
+ window.navigation?.addEventListener('navigate', (e) => e.stopImmediatePropagation())
199
+ ```
200
+
201
+ This prevents Remix from intercepting Navigation API events. Explicit frame reloads such as
202
+ `handle.frame.reload()` continue to use the frame resolver.
153
203
 
154
- The default resolver rejects non-OK responses with an error containing their status and status text.
155
- A custom `resolveFrame` may return a `Response` with any status when it wants Remix UI to render the
156
- response body.
204
+ The default resolver accepts `2xx` responses and `3xx` or `4xx` responses whose `Content-Type` includes `text/html`, ignoring case. It rejects other `3xx` or `4xx` responses and all `5xx` responses with an error containing their status and status text. A custom `resolveFrame` may return a `Response` with any status when it wants Remix UI to render the response body.
157
205
 
158
- Forms remain ordinary HTML forms before the runtime starts. Add `rmx-target` to reload a named frame, or `rmx-document` to require a full-document submission:
206
+ Forms remain ordinary HTML forms before the runtime starts. Add `data-rmx-target` to reload a named frame, or `data-rmx-document` to require a full-document submission:
159
207
 
160
208
  ```tsx
161
209
  import { Frame } from 'remix/ui'
@@ -164,7 +212,7 @@ function AccountPage() {
164
212
  return () => (
165
213
  <>
166
214
  <Frame name="account" src="/account/edit" />
167
- <form action="/account/edit" method="post" rmx-target="account">
215
+ <form action="/account/edit" method="post" data-rmx-target="account">
168
216
  <label for="display-name">Display name</label>
169
217
  <input id="display-name" name="displayName" required />
170
218
  <button type="submit">Save</button>
@@ -176,19 +224,47 @@ function AccountPage() {
176
224
 
177
225
  Native constraint validation and submitter overrides still apply. GET form values arrive in `src`; non-GET forms provide `formData`, `method`, and `encType` to the resolver. See [Frames](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#form-navigation) for targeting, history behavior, request encoding, opt-outs, and server response guidance.
178
226
 
179
- Use `rmx-history="push|replace"` on an enhanced anchor or form to control how the navigation updates history. This can override the automatic replacement used for non-GET form submissions to the current URL.
227
+ Use `data-rmx-history="push|replace"` on an enhanced anchor or form to control how the navigation updates history. This can override the automatic replacement used for non-GET form submissions to the current URL.
228
+
229
+ ## Single-page Applications
230
+
231
+ Use `render` and `run` from `remix/spa` when every route runs in the browser and returns a Remix UI
232
+ tree instead of an HTTP response body. The render middleware keeps the router's standard `Request`
233
+ to `Response` contract while associating the response with a node for the top frame to render:
234
+
235
+ ```tsx
236
+ import { createRouter } from 'remix/router'
237
+ import { render, run } from 'remix/spa'
238
+
239
+ let router = createRouter({ middleware: [render()] })
240
+
241
+ router.get('/', ({ render }) => render(<h1>Home</h1>))
242
+ router.get('/about', ({ render }) => render(<h1>About</h1>))
243
+
244
+ function LoadingPage() {
245
+ return () => <p role="status">Loading…</p>
246
+ }
247
+
248
+ let app = run(router, { fallback: <LoadingPage /> })
249
+ await app.ready()
250
+ ```
251
+
252
+ The optional `fallback` is a live Remix node displayed while the initial route loads.
253
+ `app.ready()` resolves after the initial URL has replaced it with the routed node. The runtime then
254
+ reuses frame navigation for same-origin links, forms, history traversal, redirects, cancellation,
255
+ and `rmx-target`.
180
256
 
181
257
  ## Preserving Client-Owned DOM
182
258
 
183
- Use `rmx-preserve-dom` on the smallest element whose live DOM should belong to client code after initial render, such as a custom element or third-party widget:
259
+ Use `data-rmx-preserve-dom` on the smallest element whose live DOM should belong to client code after initial render, such as a custom element or third-party widget:
184
260
 
185
261
  ```tsx
186
- <pagefind-ui data-key="search" rmx-preserve-dom>
262
+ <pagefind-ui data-rmx-key="search" data-rmx-preserve-dom>
187
263
  <button type="button">Search</button>
188
264
  </pagefind-ui>
189
265
  ```
190
266
 
191
- Remix UI still renders the element's children during SSR and still hydrates any initial client entries inside it. On later frame reloads, matched `rmx-preserve-dom` elements keep their current attributes and children instead of accepting incoming DOM updates. See [Preserving client-owned DOM](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#preserving-client-owned-dom) for guidance and caveats.
267
+ Remix UI still renders the element's children during SSR and still hydrates any initial client entries inside it. On later frame reloads, matched `data-rmx-preserve-dom` elements keep their current attributes and children instead of accepting incoming DOM updates. See [Preserving client-owned DOM](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#preserving-client-owned-dom) for guidance and caveats.
192
268
 
193
269
  ## Cascade Layers
194
270
 
@@ -33,12 +33,10 @@ let stream = renderToStream(<App />, {
33
33
  return fetchHtml(frameUrl)
34
34
  },
35
35
  async resolveClientEntry(entryId, component) {
36
- let [href, preloads] = await Promise.all([
37
- assetServer.getHref(entryId),
38
- assetServer.getPreloads(entryId),
39
- ])
36
+ let { href, importMap, preloads } = await assetServer.getScriptEntry(entryId)
40
37
  return {
41
38
  href,
39
+ importMap,
42
40
  exportName: entryId.split('#')[1] || component.name,
43
41
  preloads,
44
42
  }
@@ -59,7 +57,7 @@ return new Response(stream, {
59
57
  - **`topFrameSrc`** - Overrides the root frame URL used for `handle.frames.top.src`. This is mainly useful when calling `renderToStream()` from inside `resolveFrame()` for a nested frame render.
60
58
  - **`signal`** - Cancels pending server rendering work. Pass `request.signal` so client disconnects can stop unresolved frame work without invoking `onError` for the disconnect itself.
61
59
  - **`resolveFrame(src, target, context)`** - Called when a `<Frame>` needs its content. Return a string of HTML, a `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the URL for the frame that contains the `<Frame>`, and `context.topFrameSrc` is the outer document URL. Required if your component tree contains `<Frame>` elements.
62
- - **`resolveClientEntry(entryId, component)`** - Resolves the public module URL, export name, and optional module preload hrefs for a hydrated client entry.
60
+ - **`resolveClientEntry(entryId, component)`** - Resolves the public module URL, export name, optional import map, and optional module preload hrefs for a hydrated client entry.
63
61
  - **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
64
62
 
65
63
  When you render nested frame responses with `renderToStream()` inside `resolveFrame()`, pass `frameSrc` for the frame being rendered and carry `topFrameSrc` forward from the parent context. That preserves `handle.frames.top.src` across the whole SSR frame tree.
@@ -97,6 +95,28 @@ function ProductPage() {
97
95
 
98
96
  Components using the `css(...)` mixin through `mix` have their styles collected during rendering and emitted as a single `<style>` tag in the `<head>`. No client-side style injection is needed for server-rendered content.
99
97
 
98
+ ### Import maps
99
+
100
+ Use `<ImportMap>` when the document needs one initial import map containing both authored mappings and mappings from blocking client entries. The server merges these mappings into the component before sending the initial HTML.
101
+
102
+ Regular `<script type="importmap">` elements remain supported. The server leaves them unchanged and emits any additional client entry mappings separately, omitting entries already present in authored maps.
103
+
104
+ ```tsx
105
+ import type { Handle, RemixNode } from 'remix/ui'
106
+ import { ImportMap } from 'remix/ui/server'
107
+
108
+ function Document(handle: Handle<{ children: RemixNode }>) {
109
+ return () => (
110
+ <html>
111
+ <head>
112
+ <ImportMap value={scriptEntry.importMap} />
113
+ </head>
114
+ <body>{handle.props.children}</body>
115
+ </html>
116
+ )
117
+ }
118
+ ```
119
+
100
120
  ## See Also
101
121
 
102
122
  - [Hydration](https://github.com/remix-run/remix/blob/main/packages/ui/docs/hydration.md) - Making server-rendered components interactive on the client
@@ -35,12 +35,14 @@ let isDevelopment = process.env.NODE_ENV === 'development'
35
35
 
36
36
  let assetServer = createAssetServer({
37
37
  basePath: '/assets',
38
- fileMap: { '/app/*path': 'app/*path' },
39
38
  allowFiles: ['app/routes.ts', 'app/**/public/**'],
40
39
  allowPackages: ['remix'],
41
40
  denyFiles: ['app/**/*.test.*'],
42
41
  hmr: isDevelopment
43
- ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
42
+ ? {
43
+ channel: async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel(),
44
+ moduleImporter: 'remix/multiple-import-maps-polyfill',
45
+ }
44
46
  : undefined,
45
47
  scripts: {
46
48
  loaders: isDevelopment ? [uiHmr()] : undefined,
@@ -49,6 +51,8 @@ let assetServer = createAssetServer({
49
51
  })
50
52
  ```
51
53
 
54
+ HMR appends mappings for updated modules to the document in additional `<script type="importmap">` elements. The module importer allows browsers without native support for multiple import maps to load these updates.
55
+
52
56
  ## Direct Transforms
53
57
 
54
58
  Use the direct transform APIs when you are writing your own loader, Node module hooks, or build integration.
@@ -1,2 +0,0 @@
1
- export * from '@remix-run/static-files-middleware';
2
- //# sourceMappingURL=static-files-middleware.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"static-files-middleware.d.ts","sourceRoot":"","sources":["../src/static-files-middleware.ts"],"names":[],"mappings":"AACA,cAAc,oCAAoC,CAAA"}