@remix-run/ui 0.7.0 → 0.9.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 +90 -14
- package/dist/animation/demos/drag-release.js +5 -7
- package/dist/animation/demos/drag-release.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +9 -4
- package/dist/runtime/component.js +38 -9
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/mix.d.ts +21 -0
- package/dist/runtime/core/mix.js +128 -0
- package/dist/runtime/core/mix.js.map +1 -0
- package/dist/runtime/diff-dom.js +15 -16
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/document-reload.d.ts +2 -0
- package/dist/runtime/document-reload.js +14 -0
- package/dist/runtime/document-reload.js.map +1 -0
- package/dist/runtime/dom.d.ts +21 -11
- package/dist/runtime/event-types.d.ts +14 -0
- package/dist/runtime/event-types.js +2 -0
- package/dist/runtime/event-types.js.map +1 -0
- package/dist/runtime/frame-resolution.js +8 -0
- package/dist/runtime/frame-resolution.js.map +1 -1
- package/dist/runtime/frame.d.ts +24 -6
- package/dist/runtime/frame.js +187 -60
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/import-map-manager.d.ts +8 -0
- package/dist/runtime/import-map-manager.js +295 -0
- package/dist/runtime/import-map-manager.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.js +4 -4
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +9 -4
- package/dist/runtime/mixins/mixin.js +18 -132
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +1 -1
- package/dist/runtime/module-preloader.d.ts +2 -1
- package/dist/runtime/module-preloader.js +6 -5
- package/dist/runtime/module-preloader.js.map +1 -1
- package/dist/runtime/navigation.js +211 -34
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +18 -3
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +3 -0
- package/dist/runtime/run.js +8 -4
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +37 -11
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/spa-response.d.ts +30 -0
- package/dist/runtime/spa-response.js +47 -0
- package/dist/runtime/spa-response.js.map +1 -0
- package/dist/runtime/to-vnode.js +1 -1
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/typed-event-target.d.ts +0 -4
- package/dist/runtime/typed-event-target.js.map +1 -1
- package/dist/server/stream.d.ts +25 -2
- package/dist/server/stream.js +382 -171
- package/dist/server/stream.js.map +1 -1
- package/dist/style/stylesheet.js +2 -2
- package/dist/style/stylesheet.js.map +1 -1
- package/package.json +1 -1
- package/src/animation/demos/drag-release.ts +5 -7
- package/src/index.ts +2 -2
- package/src/runtime/component.ts +52 -14
- package/src/runtime/core/mix.ts +157 -0
- package/src/runtime/demos/readme.demo.tsx +7 -17
- package/src/runtime/diff-dom.ts +13 -15
- package/src/runtime/document-reload.ts +14 -0
- package/src/runtime/dom.ts +21 -11
- package/src/runtime/event-types.ts +27 -0
- package/src/runtime/frame-resolution.ts +9 -0
- package/src/runtime/frame.ts +216 -65
- package/src/runtime/import-map-manager.ts +369 -0
- package/src/runtime/mixins/link-mixin.ts +4 -4
- package/src/runtime/mixins/mixin.ts +44 -149
- package/src/runtime/mixins/on-mixin.ts +1 -1
- package/src/runtime/module-preloader.ts +9 -6
- package/src/runtime/navigation.ts +257 -42
- package/src/runtime/reconcile.ts +20 -4
- package/src/runtime/run.ts +13 -4
- package/src/runtime/scheduler.ts +53 -13
- package/src/runtime/spa-response.ts +56 -0
- package/src/runtime/to-vnode.ts +1 -1
- package/src/runtime/typed-event-target.ts +1 -6
- package/src/server/README.md +25 -5
- package/src/server/stream.ts +506 -202
- package/src/style/stylesheet.ts +3 -3
- package/src/test/utils.ts +1 -6
- package/dist/runtime/event-listeners.d.ts +0 -50
- package/dist/runtime/event-listeners.js +0 -31
- package/dist/runtime/event-listeners.js.map +0 -1
- package/src/runtime/event-listeners.ts +0 -171
package/src/runtime/scheduler.ts
CHANGED
|
@@ -35,13 +35,21 @@ export interface Scheduler {
|
|
|
35
35
|
dequeue(): void
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
const
|
|
38
|
+
const CASCADING_UPDATE_WARN_THRESHOLD = 50
|
|
39
|
+
const MAX_CASCADING_COMPONENT_UPDATES = 50
|
|
40
40
|
|
|
41
41
|
export type SchedulerPhaseEvent = Event & {
|
|
42
42
|
parents: ParentNode[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function getComponentName(vnode: CommittedComponentNode): string {
|
|
46
|
+
return vnode.type.name || 'Anonymous'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatComponentCounts(counts: Map<string, number>): string {
|
|
50
|
+
return Array.from(counts, ([name, count]) => `${name} x${count}`).join(', ')
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
/**
|
|
46
54
|
* Creates the DOM update scheduler used by the component runtime.
|
|
47
55
|
*
|
|
@@ -62,7 +70,10 @@ export function createScheduler(
|
|
|
62
70
|
let postCommitTasks: EmptyFn[] = []
|
|
63
71
|
let flushScheduled = false
|
|
64
72
|
let flushing = false
|
|
65
|
-
let
|
|
73
|
+
let cascadingComponentUpdateCount = 0
|
|
74
|
+
let cascadingComponentUpdateCounts = new WeakMap<CommittedComponentNode, number>()
|
|
75
|
+
let cascadingComponentNameCounts = new Map<string, number>()
|
|
76
|
+
let warnedAboutCascadingUpdates = false
|
|
66
77
|
let resetScheduled = false
|
|
67
78
|
let phaseEvents = new EventTarget()
|
|
68
79
|
let phaseListenerCounts: Record<SchedulerPhaseType, number> = {
|
|
@@ -82,11 +93,48 @@ export function createScheduler(
|
|
|
82
93
|
// Reset when control returns to the event loop while still allowing
|
|
83
94
|
// microtask-driven flushes in the same turn to count as cascading.
|
|
84
95
|
setTimeout(() => {
|
|
85
|
-
|
|
96
|
+
cascadingComponentUpdateCount = 0
|
|
97
|
+
cascadingComponentUpdateCounts = new WeakMap()
|
|
98
|
+
cascadingComponentNameCounts = new Map()
|
|
99
|
+
warnedAboutCascadingUpdates = false
|
|
86
100
|
resetScheduled = false
|
|
87
101
|
}, 0)
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
function trackCascadingUpdate(vnode: CommittedComponentNode): boolean {
|
|
105
|
+
cascadingComponentUpdateCount++
|
|
106
|
+
|
|
107
|
+
let componentName = getComponentName(vnode)
|
|
108
|
+
cascadingComponentNameCounts.set(
|
|
109
|
+
componentName,
|
|
110
|
+
(cascadingComponentNameCounts.get(componentName) ?? 0) + 1,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
let componentUpdateCount = (cascadingComponentUpdateCounts.get(vnode) ?? 0) + 1
|
|
114
|
+
cascadingComponentUpdateCounts.set(vnode, componentUpdateCount)
|
|
115
|
+
scheduleCounterReset()
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
!warnedAboutCascadingUpdates &&
|
|
119
|
+
cascadingComponentUpdateCount >= CASCADING_UPDATE_WARN_THRESHOLD
|
|
120
|
+
) {
|
|
121
|
+
warnedAboutCascadingUpdates = true
|
|
122
|
+
console.warn(
|
|
123
|
+
`${cascadingComponentUpdateCount} cascading component updates detected in one event loop turn. Consider reducing hydration regions. Components: ${formatComponentCounts(cascadingComponentNameCounts)}`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (componentUpdateCount > MAX_CASCADING_COMPONENT_UPDATES) {
|
|
128
|
+
let error = new Error(
|
|
129
|
+
`handle.update() infinite loop detected in ${componentName} after ${componentUpdateCount} cascading updates. Components: ${formatComponentCounts(cascadingComponentNameCounts)}`,
|
|
130
|
+
)
|
|
131
|
+
dispatchError(error)
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
function flush() {
|
|
91
139
|
if (flushing) return
|
|
92
140
|
flushing = true
|
|
@@ -104,15 +152,6 @@ export function createScheduler(
|
|
|
104
152
|
postCommitTasks.length > 0
|
|
105
153
|
if (!hasWork) return
|
|
106
154
|
|
|
107
|
-
cascadingUpdateCount++
|
|
108
|
-
scheduleCounterReset()
|
|
109
|
-
|
|
110
|
-
if (cascadingUpdateCount > MAX_CASCADING_UPDATES) {
|
|
111
|
-
let error = new Error('handle.update() infinite loop detected')
|
|
112
|
-
dispatchError(error)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
155
|
documentState.capture()
|
|
117
156
|
|
|
118
157
|
let updateParents = batch.size > 0 ? Array.from(new Set(batch.values())) : []
|
|
@@ -125,6 +164,7 @@ export function createScheduler(
|
|
|
125
164
|
|
|
126
165
|
for (let [vnode, domParent] of vnodes) {
|
|
127
166
|
if (ancestorIsScheduled(vnode, batch, noScheduledAncestor)) continue
|
|
167
|
+
if (!trackCascadingUpdate(vnode)) return
|
|
128
168
|
let curr = vnode._content
|
|
129
169
|
// Calculate anchor at render time from current vdom position (never stale).
|
|
130
170
|
// Needed for fragment self-updates that add children - without this, new children
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RemixNode } from './jsx.ts'
|
|
2
|
+
|
|
3
|
+
type SPAResponseData = {
|
|
4
|
+
node: RemixNode
|
|
5
|
+
redirectedTo?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let spaResponses: WeakMap<Response, SPAResponseData> | undefined
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates and finalizes responses carrying renderable Remix nodes for the SPA runtime.
|
|
12
|
+
*/
|
|
13
|
+
export const spaResponse = {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a bodyless response associated with a renderable Remix node.
|
|
16
|
+
*
|
|
17
|
+
* @param node Node to render when the response resolves a frame.
|
|
18
|
+
* @param init Standard response status, status text, and headers.
|
|
19
|
+
* @returns A response understood by the SPA runtime.
|
|
20
|
+
* @throws {TypeError} When called outside a browser environment.
|
|
21
|
+
*/
|
|
22
|
+
create(node: RemixNode, init?: ResponseInit): Response {
|
|
23
|
+
if (typeof document === 'undefined') {
|
|
24
|
+
throw new TypeError('spaResponse.create() can only be used in a browser')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let response = new Response(null, init)
|
|
28
|
+
let responses = (spaResponses ??= new WeakMap())
|
|
29
|
+
responses.set(response, { node })
|
|
30
|
+
return response
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prepares the final route response for frame resolution.
|
|
35
|
+
*
|
|
36
|
+
* @param response Final response returned by the SPA router.
|
|
37
|
+
* @param redirectedTo Final redirect URL, when the route followed redirects.
|
|
38
|
+
* @returns The same response after validating it and recording its redirect URL.
|
|
39
|
+
* @throws {TypeError} When the response was not created by `spaResponse.create()`.
|
|
40
|
+
*/
|
|
41
|
+
finalize(response: Response, redirectedTo?: string): Response {
|
|
42
|
+
let data = getSpaResponseData(response)
|
|
43
|
+
if (!data) throw new TypeError('Expected a Remix SPA response')
|
|
44
|
+
|
|
45
|
+
if (redirectedTo === undefined) {
|
|
46
|
+
delete data.redirectedTo
|
|
47
|
+
} else {
|
|
48
|
+
data.redirectedTo = redirectedTo
|
|
49
|
+
}
|
|
50
|
+
return response
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSpaResponseData(response: Response): SPAResponseData | undefined {
|
|
55
|
+
return spaResponses?.get(response)
|
|
56
|
+
}
|
package/src/runtime/to-vnode.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isEmptyChild, isPrimitiveChild, isRemixNode, normalizeChildren } from '
|
|
|
5
5
|
import type { RemixNode } from './jsx.ts'
|
|
6
6
|
import type { ElementFunction } from './element-function.ts'
|
|
7
7
|
import type { FrameProps } from './component.ts'
|
|
8
|
-
import { isMixinDescriptor } from './
|
|
8
|
+
import { isMixinDescriptor } from './core/mix.ts'
|
|
9
9
|
import {
|
|
10
10
|
isRemixElement,
|
|
11
11
|
NON_RENDER_NODE,
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An `EventTarget` subclass with typed event maps.
|
|
3
3
|
*/
|
|
4
|
-
export class TypedEventTarget<eventMap> extends EventTarget {
|
|
5
|
-
/**
|
|
6
|
-
* Phantom property that carries the event map type on instances.
|
|
7
|
-
*/
|
|
8
|
-
declare readonly __eventMap?: eventMap
|
|
9
|
-
}
|
|
4
|
+
export class TypedEventTarget<eventMap> extends EventTarget {}
|
|
10
5
|
|
|
11
6
|
/**
|
|
12
7
|
* Interface surface for {@link TypedEventTarget} with typed listener overloads.
|
package/src/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
|