@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.
Files changed (91) hide show
  1. package/README.md +90 -14
  2. package/dist/animation/demos/drag-release.js +5 -7
  3. package/dist/animation/demos/drag-release.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/runtime/component.d.ts +9 -4
  8. package/dist/runtime/component.js +38 -9
  9. package/dist/runtime/component.js.map +1 -1
  10. package/dist/runtime/core/mix.d.ts +21 -0
  11. package/dist/runtime/core/mix.js +128 -0
  12. package/dist/runtime/core/mix.js.map +1 -0
  13. package/dist/runtime/diff-dom.js +15 -16
  14. package/dist/runtime/diff-dom.js.map +1 -1
  15. package/dist/runtime/document-reload.d.ts +2 -0
  16. package/dist/runtime/document-reload.js +14 -0
  17. package/dist/runtime/document-reload.js.map +1 -0
  18. package/dist/runtime/dom.d.ts +21 -11
  19. package/dist/runtime/event-types.d.ts +14 -0
  20. package/dist/runtime/event-types.js +2 -0
  21. package/dist/runtime/event-types.js.map +1 -0
  22. package/dist/runtime/frame-resolution.js +8 -0
  23. package/dist/runtime/frame-resolution.js.map +1 -1
  24. package/dist/runtime/frame.d.ts +24 -6
  25. package/dist/runtime/frame.js +187 -60
  26. package/dist/runtime/frame.js.map +1 -1
  27. package/dist/runtime/import-map-manager.d.ts +8 -0
  28. package/dist/runtime/import-map-manager.js +295 -0
  29. package/dist/runtime/import-map-manager.js.map +1 -0
  30. package/dist/runtime/mixins/link-mixin.js +4 -4
  31. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  32. package/dist/runtime/mixins/mixin.d.ts +9 -4
  33. package/dist/runtime/mixins/mixin.js +18 -132
  34. package/dist/runtime/mixins/mixin.js.map +1 -1
  35. package/dist/runtime/mixins/on-mixin.d.ts +1 -1
  36. package/dist/runtime/module-preloader.d.ts +2 -1
  37. package/dist/runtime/module-preloader.js +6 -5
  38. package/dist/runtime/module-preloader.js.map +1 -1
  39. package/dist/runtime/navigation.js +211 -34
  40. package/dist/runtime/navigation.js.map +1 -1
  41. package/dist/runtime/reconcile.js +18 -3
  42. package/dist/runtime/reconcile.js.map +1 -1
  43. package/dist/runtime/run.d.ts +3 -0
  44. package/dist/runtime/run.js +8 -4
  45. package/dist/runtime/run.js.map +1 -1
  46. package/dist/runtime/scheduler.js +37 -11
  47. package/dist/runtime/scheduler.js.map +1 -1
  48. package/dist/runtime/spa-response.d.ts +30 -0
  49. package/dist/runtime/spa-response.js +47 -0
  50. package/dist/runtime/spa-response.js.map +1 -0
  51. package/dist/runtime/to-vnode.js +1 -1
  52. package/dist/runtime/to-vnode.js.map +1 -1
  53. package/dist/runtime/typed-event-target.d.ts +0 -4
  54. package/dist/runtime/typed-event-target.js.map +1 -1
  55. package/dist/server/stream.d.ts +25 -2
  56. package/dist/server/stream.js +382 -171
  57. package/dist/server/stream.js.map +1 -1
  58. package/dist/style/stylesheet.js +2 -2
  59. package/dist/style/stylesheet.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/animation/demos/drag-release.ts +5 -7
  62. package/src/index.ts +2 -2
  63. package/src/runtime/component.ts +52 -14
  64. package/src/runtime/core/mix.ts +157 -0
  65. package/src/runtime/demos/readme.demo.tsx +7 -17
  66. package/src/runtime/diff-dom.ts +13 -15
  67. package/src/runtime/document-reload.ts +14 -0
  68. package/src/runtime/dom.ts +21 -11
  69. package/src/runtime/event-types.ts +27 -0
  70. package/src/runtime/frame-resolution.ts +9 -0
  71. package/src/runtime/frame.ts +216 -65
  72. package/src/runtime/import-map-manager.ts +369 -0
  73. package/src/runtime/mixins/link-mixin.ts +4 -4
  74. package/src/runtime/mixins/mixin.ts +44 -149
  75. package/src/runtime/mixins/on-mixin.ts +1 -1
  76. package/src/runtime/module-preloader.ts +9 -6
  77. package/src/runtime/navigation.ts +257 -42
  78. package/src/runtime/reconcile.ts +20 -4
  79. package/src/runtime/run.ts +13 -4
  80. package/src/runtime/scheduler.ts +53 -13
  81. package/src/runtime/spa-response.ts +56 -0
  82. package/src/runtime/to-vnode.ts +1 -1
  83. package/src/runtime/typed-event-target.ts +1 -6
  84. package/src/server/README.md +25 -5
  85. package/src/server/stream.ts +506 -202
  86. package/src/style/stylesheet.ts +3 -3
  87. package/src/test/utils.ts +1 -6
  88. package/dist/runtime/event-listeners.d.ts +0 -50
  89. package/dist/runtime/event-listeners.js +0 -31
  90. package/dist/runtime/event-listeners.js.map +0 -1
  91. package/src/runtime/event-listeners.ts +0 -171
@@ -35,13 +35,21 @@ export interface Scheduler {
35
35
  dequeue(): void
36
36
  }
37
37
 
38
- // Protect against infinite cascading updates (e.g. handle.update() during render)
39
- const MAX_CASCADING_UPDATES = 50
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 cascadingUpdateCount = 0
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
- cascadingUpdateCount = 0
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
+ }
@@ -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 './mixins/mixin.ts'
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.
@@ -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