remix 3.0.0-beta.5 → 3.0.0-beta.9
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 +4 -2
- package/dist/assets/types/hmr.d.ts +2 -0
- package/dist/cli-entry.js +1 -1
- package/dist/data-table/cli.d.ts +2 -0
- package/dist/data-table/cli.d.ts.map +1 -0
- package/{src/static-middleware.ts → dist/data-table/cli.js} +1 -1
- package/dist/node-hmr/runtime.d.ts +2 -0
- package/dist/node-hmr/runtime.d.ts.map +1 -0
- package/dist/node-hmr/runtime.js +2 -0
- package/dist/node-hmr/types.d.ts +2 -0
- package/dist/node-hmr.d.ts +2 -0
- package/dist/node-hmr.d.ts.map +1 -0
- package/dist/{static-middleware.js → node-hmr.js} +1 -1
- package/dist/static-files-middleware.d.ts +2 -0
- package/dist/static-files-middleware.d.ts.map +1 -0
- package/dist/static-files-middleware.js +2 -0
- package/dist/ui/dev/refresh.d.ts +2 -0
- package/dist/ui/dev/refresh.d.ts.map +1 -0
- package/dist/ui/dev/refresh.js +2 -0
- package/dist/ui-hmr/assets.d.ts +2 -0
- package/dist/ui-hmr/assets.d.ts.map +1 -0
- package/dist/ui-hmr/assets.js +2 -0
- package/dist/ui-hmr/node.d.ts +3 -0
- package/dist/ui-hmr/node.d.ts.map +1 -0
- package/dist/ui-hmr/node.js +3 -0
- package/dist/ui-hmr/runtime/browser.d.ts +2 -0
- package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/browser.js +2 -0
- package/dist/ui-hmr/runtime/server.d.ts +2 -0
- package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/server.js +2 -0
- package/dist/ui-hmr.d.ts +2 -0
- package/dist/ui-hmr.d.ts.map +1 -0
- package/dist/ui-hmr.js +2 -0
- package/package.json +80 -128
- package/src/assets/README.md +322 -56
- package/src/assets/types/hmr.d.ts +2 -0
- package/src/cli/README.md +105 -1
- package/src/cookie/README.md +4 -4
- package/src/data-table/README.md +202 -68
- package/src/data-table/cli.ts +2 -0
- package/src/data-table-mysql/README.md +22 -18
- package/src/data-table-postgres/README.md +14 -14
- package/src/data-table-sqlite/README.md +38 -20
- package/src/fetch-proxy/README.md +25 -0
- package/src/node-fetch-server/README.md +11 -11
- package/src/node-hmr/README.md +307 -0
- package/src/node-hmr/runtime.ts +2 -0
- package/src/node-hmr/types.d.ts +2 -0
- package/src/node-hmr.ts +2 -0
- package/src/route-pattern/README.md +91 -8
- package/src/session-middleware/README.md +9 -7
- package/src/{static-middleware → static-files-middleware}/README.md +2 -2
- package/src/static-files-middleware.ts +2 -0
- package/src/test/README.md +161 -115
- package/src/ui/README.md +112 -1
- package/src/ui/dev/refresh.ts +2 -0
- package/src/ui/server/README.md +13 -0
- package/src/ui/test/README.md +151 -60
- package/src/ui-hmr/README.md +119 -0
- package/src/ui-hmr/assets.ts +2 -0
- package/src/ui-hmr/node.ts +3 -0
- package/src/ui-hmr/runtime/browser.ts +2 -0
- package/src/ui-hmr/runtime/server.ts +2 -0
- package/src/ui-hmr.ts +2 -0
- package/dist/static-middleware.d.ts +0 -2
- package/dist/static-middleware.d.ts.map +0 -1
package/src/ui/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Runtime UI primitives for Remix apps, including the component runtime, server re
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Component runtime APIs for rendering, hydration, frame navigation, and JSX
|
|
7
|
+
- Component runtime APIs for rendering, hydration, link and form frame navigation, and JSX
|
|
8
8
|
- Server rendering APIs for streaming Remix UI trees and frames
|
|
9
9
|
- `mix` composition with event, ref, CSS, and animation helpers
|
|
10
10
|
- Headless behavior primitives for controls such as menus, listboxes, popovers, selects, and comboboxes
|
|
@@ -79,6 +79,117 @@ function Actions() {
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
## Frame Navigation
|
|
83
|
+
|
|
84
|
+
`run()` progressively enhances same-origin links and forms using a default `resolveFrame` that
|
|
85
|
+
fetches the frame source:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { run } from 'remix/ui'
|
|
89
|
+
|
|
90
|
+
let app = run({
|
|
91
|
+
async loadModule(moduleUrl, exportName) {
|
|
92
|
+
let mod = await import(moduleUrl)
|
|
93
|
+
return mod[exportName]
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await app.ready()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The default resolver is equivalent to:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
async function resolveFrame(src, options) {
|
|
104
|
+
let response = await fetch(src, {
|
|
105
|
+
body: getRequestBody(options),
|
|
106
|
+
headers: { Accept: 'text/html' },
|
|
107
|
+
method: options?.method,
|
|
108
|
+
signal: options?.signal,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`Failed to resolve frame: ${response.status} ${response.statusText}`.trimEnd())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return response
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getRequestBody(options) {
|
|
119
|
+
let formData = options?.formData
|
|
120
|
+
if (!formData || options?.method?.toLowerCase() === 'get') return
|
|
121
|
+
|
|
122
|
+
if (options?.encType === 'text/plain') {
|
|
123
|
+
let body = ''
|
|
124
|
+
for (let [name, value] of formData) {
|
|
125
|
+
name = normalizeLineBreaks(name)
|
|
126
|
+
value = normalizeLineBreaks(typeof value === 'string' ? value : value.name)
|
|
127
|
+
body += `${name}=${value}\r\n`
|
|
128
|
+
}
|
|
129
|
+
return new Blob([body], { type: 'text/plain' })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options?.encType !== 'application/x-www-form-urlencoded') return formData
|
|
133
|
+
|
|
134
|
+
let body = new URLSearchParams()
|
|
135
|
+
for (let [name, value] of formData) {
|
|
136
|
+
body.append(name, typeof value === 'string' ? value : value.name)
|
|
137
|
+
}
|
|
138
|
+
return body
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeLineBreaks(value) {
|
|
142
|
+
return value.replace(/\r\n|\r|\n/g, '\r\n')
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The default resolver requests HTML. GET form values are already encoded in `src`;
|
|
147
|
+
`application/x-www-form-urlencoded` submissions use `URLSearchParams`, `text/plain` submissions use
|
|
148
|
+
CRLF-delimited text, and `multipart/form-data` submissions use `FormData`. Pass a custom
|
|
149
|
+
`resolveFrame` when the server requires additional headers, another body encoding, or a different
|
|
150
|
+
response policy.
|
|
151
|
+
|
|
152
|
+
Add `rmx-document` to a link or form to leave its navigation to the browser.
|
|
153
|
+
|
|
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.
|
|
157
|
+
|
|
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:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { Frame } from 'remix/ui'
|
|
162
|
+
|
|
163
|
+
function AccountPage() {
|
|
164
|
+
return () => (
|
|
165
|
+
<>
|
|
166
|
+
<Frame name="account" src="/account/edit" />
|
|
167
|
+
<form action="/account/edit" method="post" rmx-target="account">
|
|
168
|
+
<label for="display-name">Display name</label>
|
|
169
|
+
<input id="display-name" name="displayName" required />
|
|
170
|
+
<button type="submit">Save</button>
|
|
171
|
+
</form>
|
|
172
|
+
</>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
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
|
+
|
|
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.
|
|
180
|
+
|
|
181
|
+
## Preserving Client-Owned DOM
|
|
182
|
+
|
|
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:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<pagefind-ui data-key="search" rmx-preserve-dom>
|
|
187
|
+
<button type="button">Search</button>
|
|
188
|
+
</pagefind-ui>
|
|
189
|
+
```
|
|
190
|
+
|
|
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.
|
|
192
|
+
|
|
82
193
|
## Cascade Layers
|
|
83
194
|
|
|
84
195
|
Remix UI emits generated `css(...)` rules under the `rmx` cascade layer. Unlayered CSS outranks layered CSS, so use explicit layer order when mixing Remix UI with global styles.
|
package/src/ui/server/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Renders a component tree to a streaming response. The initial HTML is sent immed
|
|
|
23
23
|
|
|
24
24
|
```tsx
|
|
25
25
|
import { renderToStream } from 'remix/ui/server'
|
|
26
|
+
import { assetServer } from './assets.ts'
|
|
26
27
|
|
|
27
28
|
let stream = renderToStream(<App />, {
|
|
28
29
|
frameSrc: request.url,
|
|
@@ -31,6 +32,17 @@ let stream = renderToStream(<App />, {
|
|
|
31
32
|
let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
|
|
32
33
|
return fetchHtml(frameUrl)
|
|
33
34
|
},
|
|
35
|
+
async resolveClientEntry(entryId, component) {
|
|
36
|
+
let [href, preloads] = await Promise.all([
|
|
37
|
+
assetServer.getHref(entryId),
|
|
38
|
+
assetServer.getPreloads(entryId),
|
|
39
|
+
])
|
|
40
|
+
return {
|
|
41
|
+
href,
|
|
42
|
+
exportName: entryId.split('#')[1] || component.name,
|
|
43
|
+
preloads,
|
|
44
|
+
}
|
|
45
|
+
},
|
|
34
46
|
onError(error) {
|
|
35
47
|
console.error(error)
|
|
36
48
|
},
|
|
@@ -47,6 +59,7 @@ return new Response(stream, {
|
|
|
47
59
|
- **`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.
|
|
48
60
|
- **`signal`** - Cancels pending server rendering work. Pass `request.signal` so client disconnects can stop unresolved frame work without invoking `onError` for the disconnect itself.
|
|
49
61
|
- **`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.
|
|
50
63
|
- **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.
|
|
51
64
|
|
|
52
65
|
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.
|
package/src/ui/test/README.md
CHANGED
|
@@ -1,104 +1,195 @@
|
|
|
1
1
|
# Test
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `render` from `remix/ui/test` as the primary way to test components in a live browser through a `remix/test` "browser" test (`*.test.browser.tsx`). It creates a DOM container, renders and flushes the initial component tree, and returns helpers for querying, interacting with, and cleaning up the rendered output.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
The main use case is flushing after events that call `handle.update()`. Since updates are asynchronous, you need to flush to ensure the DOM reflects the changes:
|
|
5
|
+
## Rendering Components
|
|
8
6
|
|
|
9
7
|
```tsx
|
|
8
|
+
import { expect } from 'remix/assert'
|
|
9
|
+
import { describe, it } from 'remix/test'
|
|
10
|
+
import { on, type Handle } from 'remix/ui'
|
|
11
|
+
import { render } from 'remix/ui/test'
|
|
12
|
+
|
|
10
13
|
function Counter(handle: Handle) {
|
|
11
14
|
let count = 0
|
|
12
15
|
|
|
13
16
|
return () => (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<div>
|
|
18
|
+
<button
|
|
19
|
+
data-action="increment"
|
|
20
|
+
mix={[
|
|
21
|
+
on('click', () => {
|
|
22
|
+
count++
|
|
23
|
+
handle.update()
|
|
24
|
+
}),
|
|
25
|
+
]}
|
|
26
|
+
>
|
|
27
|
+
Increment
|
|
28
|
+
</button>
|
|
29
|
+
<output data-testid="count">{count}</output>
|
|
30
|
+
</div>
|
|
24
31
|
)
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let
|
|
34
|
+
describe('Counter', () => {
|
|
35
|
+
it('increments the count', async (t) => {
|
|
36
|
+
let { $, act, cleanup } = render(<Counter />)
|
|
37
|
+
t.after(cleanup)
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
root.flush() // Ensure initial render completes
|
|
39
|
+
expect($('[data-testid="count"]')?.textContent).toBe('0')
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
button.click() // Triggers handle.update()
|
|
36
|
-
root.flush() // Flush to apply the update
|
|
41
|
+
await act(() => $('[data-action="increment"]')?.click())
|
|
37
42
|
|
|
38
|
-
expect(
|
|
43
|
+
expect($('[data-testid="count"]')?.textContent).toBe('1')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
The initial render is already flushed, so the DOM and event listeners are ready when `render`
|
|
49
|
+
returns. Wrap interactions that may update component state in `act` and always await it. `act`
|
|
50
|
+
waits for the callback and then flushes pending component updates before the next assertion.
|
|
51
|
+
|
|
52
|
+
### Testing Async Operations
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
When an interaction starts an async operation, await that operation inside `act` so updates queued
|
|
55
|
+
after it resolves are flushed before making assertions:
|
|
44
56
|
|
|
45
57
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
root.flush() // Event listeners now attached
|
|
58
|
+
function UserLoader(handle: Handle<{ loadUser(): Promise<{ name: string }> }>) {
|
|
59
|
+
let name = 'No user loaded'
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
return () => (
|
|
62
|
+
<div>
|
|
63
|
+
<button
|
|
64
|
+
mix={[
|
|
65
|
+
on('click', async () => {
|
|
66
|
+
let user = await handle.props.loadUser()
|
|
67
|
+
name = user.name
|
|
68
|
+
handle.update()
|
|
69
|
+
}),
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
Load user
|
|
73
|
+
</button>
|
|
74
|
+
<output>{name}</output>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it('loads a user', async (t) => {
|
|
80
|
+
let userPromise = Promise.resolve({ name: 'Ada' })
|
|
81
|
+
let { $, act, cleanup } = render(<UserLoader loadUser={() => userPromise} />)
|
|
82
|
+
t.after(cleanup)
|
|
83
|
+
|
|
84
|
+
await act(async () => {
|
|
85
|
+
$('button')?.click()
|
|
86
|
+
await userPromise
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect($('output')?.textContent).toBe('Ada')
|
|
90
|
+
})
|
|
52
91
|
```
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
### Querying Rendered Output
|
|
55
94
|
|
|
56
|
-
|
|
95
|
+
The returned query helpers are scoped to the rendered container so you don't have to worry about false positives outside of the rendered component:
|
|
57
96
|
|
|
58
97
|
```tsx
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (signal.aborted) return
|
|
66
|
-
data = json.value
|
|
67
|
-
handle.update()
|
|
68
|
-
})
|
|
98
|
+
let { $, $$, container, cleanup } = render(
|
|
99
|
+
<nav>
|
|
100
|
+
<a href="/one">One</a>
|
|
101
|
+
<a href="/two">Two</a>
|
|
102
|
+
</nav>,
|
|
103
|
+
)
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
let nav = $('nav') // container.querySelector('nav')
|
|
106
|
+
let links = $$('a') // container.querySelectorAll('a')
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
root.flush()
|
|
108
|
+
expect(nav).toBeTruthy()
|
|
109
|
+
expect(links).toHaveLength(2)
|
|
110
|
+
expect(container.textContent).toContain('One')
|
|
77
111
|
|
|
78
|
-
|
|
112
|
+
cleanup()
|
|
113
|
+
```
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
root.flush()
|
|
115
|
+
Use `$` for the first matching element, `$$` for all matching elements, and `container` when a
|
|
116
|
+
DOM assertion does not fit a selector.
|
|
83
117
|
|
|
84
|
-
|
|
118
|
+
### Cleaning Up
|
|
119
|
+
|
|
120
|
+
Register `cleanup` with the test context so it always runs, including when an assertion fails:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
it('renders a dialog', (t) => {
|
|
124
|
+
let { $, cleanup } = render(<Dialog />)
|
|
125
|
+
t.after(cleanup)
|
|
126
|
+
|
|
127
|
+
expect($('[role="dialog"]')).toBeTruthy()
|
|
128
|
+
})
|
|
85
129
|
```
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
`cleanup` disposes the root and removes its container from the document. You can call it directly
|
|
132
|
+
when a test needs to assert cleanup behavior.
|
|
133
|
+
|
|
134
|
+
### Rendering Into an Existing Container
|
|
88
135
|
|
|
89
|
-
|
|
136
|
+
Pass a container when the test needs a specific DOM context:
|
|
90
137
|
|
|
91
138
|
```tsx
|
|
139
|
+
let container = document.createElement('section')
|
|
140
|
+
document.body.appendChild(container)
|
|
141
|
+
|
|
142
|
+
let result = render(<MyComponent />, { container })
|
|
143
|
+
|
|
144
|
+
expect(result.container).toBe(container)
|
|
145
|
+
result.cleanup()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Advanced: Flushing Manually
|
|
149
|
+
|
|
150
|
+
Most component tests should use `render` and `act`. For lower-level runtime tests that need direct
|
|
151
|
+
control over rendering or scheduling, use the returned `root` and call `root.flush()` after an
|
|
152
|
+
operation that queues work:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
let { root, container, cleanup } = render(<MyComponent value="first" />)
|
|
156
|
+
|
|
157
|
+
root.render(<MyComponent value="second" />)
|
|
158
|
+
root.flush()
|
|
159
|
+
|
|
160
|
+
expect(container.textContent).toContain('second')
|
|
161
|
+
cleanup()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
You can also create and manage a root directly:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { createRoot } from 'remix/ui'
|
|
168
|
+
|
|
169
|
+
let container = document.createElement('div')
|
|
92
170
|
let root = createRoot(container)
|
|
171
|
+
|
|
93
172
|
root.render(<MyComponent />)
|
|
94
|
-
root.flush()
|
|
173
|
+
root.flush() // Complete the initial render and attach event listeners
|
|
95
174
|
|
|
96
|
-
|
|
97
|
-
|
|
175
|
+
container.querySelector('button')?.click()
|
|
176
|
+
root.flush() // Apply updates queued by the interaction
|
|
98
177
|
|
|
99
|
-
// Remove and verify cleanup
|
|
100
178
|
root.dispose()
|
|
101
|
-
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Manual flushing is also useful after an async operation resolves outside an `act` callback:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
root.render(<AsyncLoader />)
|
|
185
|
+
root.flush()
|
|
186
|
+
|
|
187
|
+
expect(container.textContent).toBe('Loading...')
|
|
188
|
+
|
|
189
|
+
await waitForFetch()
|
|
190
|
+
root.flush()
|
|
191
|
+
|
|
192
|
+
expect(container.textContent).toBe('Expected data')
|
|
102
193
|
```
|
|
103
194
|
|
|
104
195
|
## See Also
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ui-hmr
|
|
2
|
+
|
|
3
|
+
Hot module replacement transforms for Remix UI components, with integrations for Node and `remix/assets`.
|
|
4
|
+
|
|
5
|
+
`ui-hmr` rewrites supported Remix UI component modules so they can use the standard `import.meta.hot` APIs provided by packages like [`assets`](https://github.com/remix-run/remix/tree/main/packages/assets) and [`node-hmr`](https://github.com/remix-run/remix/tree/main/packages/node-hmr).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Stable Components** - Keep component identity stable while swapping implementations
|
|
10
|
+
- **Remount Fallbacks** - Mark components stale when their setup scope changes
|
|
11
|
+
- **Node Module Hooks** - Transform server component modules through Node's module customization hooks API
|
|
12
|
+
- **Assets Loader** - Transform component modules for the browser through `remix/assets`
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i remix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Use `remix/ui-hmr/node` as a Node import hook for server modules:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
node --import remix/node-tsx --import remix/ui-hmr/node ./server.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `uiHmr()` from `remix/ui-hmr/assets` with `remix/assets` for browser modules:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createAssetServer } from 'remix/assets'
|
|
32
|
+
import { uiHmr } from 'remix/ui-hmr/assets'
|
|
33
|
+
|
|
34
|
+
let isDevelopment = process.env.NODE_ENV === 'development'
|
|
35
|
+
|
|
36
|
+
let assetServer = createAssetServer({
|
|
37
|
+
basePath: '/assets',
|
|
38
|
+
fileMap: { '/app/*path': 'app/*path' },
|
|
39
|
+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
|
|
40
|
+
allowPackages: ['remix'],
|
|
41
|
+
denyFiles: ['app/**/*.test.*'],
|
|
42
|
+
hmr: isDevelopment
|
|
43
|
+
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
|
|
44
|
+
: undefined,
|
|
45
|
+
scripts: {
|
|
46
|
+
loaders: isDevelopment ? [uiHmr()] : undefined,
|
|
47
|
+
},
|
|
48
|
+
watch: isDevelopment,
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Direct Transforms
|
|
53
|
+
|
|
54
|
+
Use the direct transform APIs when you are writing your own loader, Node module hooks, or build integration.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { transformComponentsForBrowser } from 'remix/ui-hmr'
|
|
58
|
+
|
|
59
|
+
let result = transformComponentsForBrowser(source, {
|
|
60
|
+
importSource: 'remix',
|
|
61
|
+
moduleUrl: '/assets/app/routes.tsx',
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (result.transformed) {
|
|
65
|
+
console.log(result.componentNames)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`transformComponentsForBrowser(source, options)` rewrites browser component modules and emits `import.meta.hot.accept()` code for browser updates. The direct transform uses `importSource` to derive imports for the UI refresh runtime and browser HMR runtime.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { transformComponentsForServer } from 'remix/ui-hmr'
|
|
73
|
+
|
|
74
|
+
let result = transformComponentsForServer(source, {
|
|
75
|
+
importSource: 'remix',
|
|
76
|
+
moduleUrl: 'file:///app/routes.tsx',
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`transformComponentsForServer(source, options)` rewrites server component modules and registers updated component implementations for the current module URL. The direct transform uses `importSource` to derive imports for the server HMR runtime.
|
|
81
|
+
|
|
82
|
+
Both transforms return:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
type ComponentsHmrTransformResult = {
|
|
86
|
+
code: string
|
|
87
|
+
componentNames: string[]
|
|
88
|
+
map: string | null
|
|
89
|
+
transformed: boolean
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pass `sourceMap: true` to generate a source map.
|
|
94
|
+
|
|
95
|
+
Use `importSource` to define where injected imports come from, typically either `remix` or `@remix-run`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
transformComponentsForBrowser(source, {
|
|
99
|
+
importSource: 'remix',
|
|
100
|
+
moduleUrl: '/assets/app/routes.tsx',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
transformComponentsForServer(source, {
|
|
104
|
+
importSource: 'remix',
|
|
105
|
+
moduleUrl: 'file:///app/routes.tsx',
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`importSource: 'remix'` generates imports from `remix/ui` and `remix/ui-hmr`. `importSource: '@remix-run'` generates imports from `@remix-run/ui` and `@remix-run/ui-hmr`. Custom import sources follow the same nested import layout.
|
|
110
|
+
|
|
111
|
+
## Related Packages
|
|
112
|
+
|
|
113
|
+
- [`assets`](https://github.com/remix-run/remix/tree/main/packages/assets) - Runs loaders while compiling assets
|
|
114
|
+
- [`node-hmr`](https://github.com/remix-run/remix/tree/main/packages/node-hmr) - Provides the server-side `import.meta.hot` runtime
|
|
115
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) - Component APIs transformed by `ui-hmr`
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
package/src/ui-hmr.ts
ADDED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"static-middleware.d.ts","sourceRoot":"","sources":["../src/static-middleware.ts"],"names":[],"mappings":"AACA,cAAc,8BAA8B,CAAA"}
|