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/README.md +2 -0
- package/dist/multiple-import-maps-polyfill.d.ts +2 -0
- package/dist/multiple-import-maps-polyfill.d.ts.map +1 -0
- package/dist/multiple-import-maps-polyfill.js +2 -0
- package/dist/spa.d.ts +2 -0
- package/dist/spa.d.ts.map +1 -0
- package/{src/static-files-middleware.ts → dist/spa.js} +1 -1
- package/dist/static-middleware.d.ts +2 -0
- package/dist/static-middleware.d.ts.map +1 -0
- package/dist/{static-files-middleware.js → static-middleware.js} +1 -1
- package/package.json +44 -33
- package/schema/remix.json +290 -0
- package/src/assets/README.md +185 -104
- package/src/auth/README.md +51 -15
- package/src/cli/README.md +52 -3
- package/src/data-table/README.md +18 -5
- package/src/fetch-router/README.md +39 -0
- package/src/multiple-import-maps-polyfill/README.md +107 -0
- package/src/multiple-import-maps-polyfill.ts +2 -0
- package/src/node-hmr/README.md +0 -1
- package/src/render-middleware/README.md +71 -56
- package/src/spa/README.md +96 -0
- package/src/spa.ts +2 -0
- package/src/{static-files-middleware → static-middleware}/README.md +1 -1
- package/src/static-middleware.ts +2 -0
- package/src/test/README.md +1 -1
- package/src/ui/README.md +90 -14
- package/src/ui/server/README.md +25 -5
- package/src/ui-hmr/README.md +6 -2
- package/dist/static-files-middleware.d.ts +0 -2
- package/dist/static-files-middleware.d.ts.map +0 -1
package/src/ui/README.md
CHANGED
|
@@ -79,10 +79,9 @@ function Actions() {
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Client Entry Loading
|
|
83
83
|
|
|
84
|
-
`run()`
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/ui/server/README.md
CHANGED
|
@@ -33,12 +33,10 @@ let stream = renderToStream(<App />, {
|
|
|
33
33
|
return fetchHtml(frameUrl)
|
|
34
34
|
},
|
|
35
35
|
async resolveClientEntry(entryId, component) {
|
|
36
|
-
let
|
|
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
|
package/src/ui-hmr/README.md
CHANGED
|
@@ -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
|
-
?
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"static-files-middleware.d.ts","sourceRoot":"","sources":["../src/static-files-middleware.ts"],"names":[],"mappings":"AACA,cAAc,oCAAoC,CAAA"}
|