@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
@@ -0,0 +1,369 @@
1
+ type ImportMap = {
2
+ imports?: ImportMapImports
3
+ scopes?: Record<string, ImportMapImports>
4
+ integrity?: Record<string, string>
5
+ }
6
+
7
+ type ImportMapAddress = string | null
8
+ type ImportMapImports = Record<string, ImportMapAddress>
9
+ type InstalledImportMapEntry = { href: ImportMapAddress; normalizedHref: ImportMapAddress }
10
+
11
+ type InstalledImportMap = {
12
+ imports: Map<string, InstalledImportMapEntry>
13
+ scopes: Map<string, Map<string, InstalledImportMapEntry>>
14
+ integrity: Map<string, string>
15
+ }
16
+
17
+ interface ImportMapManager {
18
+ consumeImportMaps(source: ParentNode): 'ready' | 'conflict' | 'blocked'
19
+ disconnect(): void
20
+ shouldPreserveHeadNode(node: Node): boolean
21
+ }
22
+
23
+ const MANAGED_IMPORT_MAP_SELECTOR = 'script[data-rmx-import-map][type="importmap"]'
24
+ const IMPORT_MAP_SELECTOR = 'script[type="importmap"]'
25
+ const importMapManagers = new WeakMap<Document, ImportMapManager>()
26
+
27
+ class ImportMapConflictError extends Error {}
28
+
29
+ export function getDocumentImportMapManager(doc: Document): ImportMapManager {
30
+ let manager = importMapManagers.get(doc)
31
+ if (!manager) {
32
+ manager = createImportMapManager(doc)
33
+ importMapManagers.set(doc, manager)
34
+ }
35
+ return manager
36
+ }
37
+
38
+ export function resetDocumentImportMapManager(doc: Document): void {
39
+ importMapManagers.get(doc)?.disconnect()
40
+ importMapManagers.delete(doc)
41
+ }
42
+
43
+ function createImportMapManager(doc: Document): ImportMapManager {
44
+ let nonce = doc.head.querySelector<HTMLScriptElement>(MANAGED_IMPORT_MAP_SELECTOR)?.nonce
45
+ let installedImportMap = createInstalledImportMap()
46
+ let processedScripts = new WeakSet<HTMLScriptElement>()
47
+ let conflicted = false
48
+
49
+ function processImportMap(script: HTMLScriptElement): void {
50
+ if (processedScripts.has(script)) return
51
+ processedScripts.add(script)
52
+
53
+ let importMap = parseImportMap(script.textContent ?? '')
54
+ if (importMap) mergeInstalledImportMap(installedImportMap, importMap, script.baseURI)
55
+ }
56
+
57
+ function processImportMaps(): void {
58
+ for (let script of doc.querySelectorAll<HTMLScriptElement>(IMPORT_MAP_SELECTOR)) {
59
+ processImportMap(script)
60
+ }
61
+ }
62
+
63
+ function processMutations(mutations: MutationRecord[]): void {
64
+ for (let mutation of mutations) {
65
+ if (mutation.type !== 'childList') continue
66
+ for (let node of mutation.addedNodes) {
67
+ if (node instanceof HTMLScriptElement && node.matches(IMPORT_MAP_SELECTOR)) {
68
+ processImportMap(node)
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ let observer = new MutationObserver(processMutations)
75
+ observer.observe(doc, { childList: true })
76
+ observer.observe(doc.head, { childList: true })
77
+ processImportMaps()
78
+
79
+ return {
80
+ consumeImportMaps(source) {
81
+ if (conflicted) return 'blocked'
82
+ processMutations(observer.takeRecords())
83
+ processImportMaps()
84
+ let scripts = Array.from(
85
+ source.querySelectorAll<HTMLScriptElement>(MANAGED_IMPORT_MAP_SELECTOR),
86
+ )
87
+ if (scripts.length === 0) return 'ready'
88
+
89
+ let pendingImportMap: InstalledImportMap = {
90
+ imports: new Map(installedImportMap.imports),
91
+ scopes: new Map(
92
+ Array.from(installedImportMap.scopes, ([scope, imports]) => [scope, new Map(imports)]),
93
+ ),
94
+ integrity: new Map(installedImportMap.integrity),
95
+ }
96
+ let deltas: ImportMap[] = []
97
+ let baseUrl = doc.baseURI
98
+ try {
99
+ for (let script of scripts) {
100
+ let importMap = parseImportMap(script.textContent ?? '')
101
+ if (!importMap) continue
102
+ let delta = getImportMapDelta(pendingImportMap, importMap, baseUrl)
103
+ if (delta) {
104
+ deltas.push(delta)
105
+ mergeInstalledImportMap(pendingImportMap, delta, baseUrl)
106
+ }
107
+ }
108
+ } catch (error) {
109
+ if (!(error instanceof ImportMapConflictError)) throw error
110
+ console.warn(error.message)
111
+ conflicted = true
112
+ return 'conflict'
113
+ }
114
+
115
+ for (let delta of deltas) {
116
+ let installedScript = appendImportMapScript(doc, delta, nonce)
117
+ processedScripts.add(installedScript)
118
+ }
119
+ installedImportMap = pendingImportMap
120
+ for (let script of scripts) script.remove()
121
+ return 'ready'
122
+ },
123
+ disconnect() {
124
+ observer.disconnect()
125
+ },
126
+ shouldPreserveHeadNode(node) {
127
+ return (
128
+ node.isConnected &&
129
+ node instanceof HTMLScriptElement &&
130
+ node.matches(MANAGED_IMPORT_MAP_SELECTOR)
131
+ )
132
+ },
133
+ }
134
+ }
135
+
136
+ function createInstalledImportMap(): InstalledImportMap {
137
+ return {
138
+ imports: new Map(),
139
+ scopes: new Map(),
140
+ integrity: new Map(),
141
+ }
142
+ }
143
+
144
+ function getImportMapDelta(
145
+ installedImportMap: InstalledImportMap,
146
+ importMap: ImportMap,
147
+ baseUrl: string,
148
+ ): ImportMap | undefined {
149
+ let imports = getImportMapImportsDelta(installedImportMap.imports, importMap.imports, baseUrl)
150
+ let scopes = getImportMapScopesDelta(installedImportMap, importMap.scopes, baseUrl)
151
+ let integrity = getImportMapIntegrityDelta(
152
+ installedImportMap.integrity,
153
+ importMap.integrity,
154
+ baseUrl,
155
+ )
156
+ if (!imports && !scopes && !integrity) return undefined
157
+ return {
158
+ ...(imports ? { imports } : null),
159
+ ...(scopes ? { scopes } : null),
160
+ ...(integrity ? { integrity } : null),
161
+ }
162
+ }
163
+
164
+ function getImportMapImportsDelta(
165
+ installedImports: Map<string, InstalledImportMapEntry>,
166
+ imports: ImportMapImports | undefined,
167
+ baseUrl: string,
168
+ scope?: string,
169
+ ): ImportMapImports | undefined {
170
+ if (!imports) return undefined
171
+
172
+ let delta: ImportMapImports = {}
173
+ for (let [specifier, href] of Object.entries(imports)) {
174
+ let normalizedSpecifier = normalizeImportMapSpecifier(specifier, baseUrl)
175
+ if (normalizedSpecifier === null) continue
176
+ let normalizedHref = normalizeImportMapAddress(href, baseUrl)
177
+ let installedEntry = installedImports.get(normalizedSpecifier)
178
+ if (installedEntry?.normalizedHref === normalizedHref) continue
179
+ if (installedEntry) {
180
+ let scopeDescription = scope ? ` in scope "${scope}"` : ''
181
+ throw new ImportMapConflictError(
182
+ `[remix] Reloading page after import map conflict for "${specifier}"${scopeDescription}: ` +
183
+ `${formatImportMapAddress(installedEntry.href)} is already installed, but the new map points to ${formatImportMapAddress(href)}`,
184
+ )
185
+ }
186
+ delta[specifier] = href
187
+ }
188
+
189
+ return Object.keys(delta).length > 0 ? delta : undefined
190
+ }
191
+
192
+ function getImportMapScopesDelta(
193
+ installedImportMap: InstalledImportMap,
194
+ scopes: Record<string, ImportMapImports> | undefined,
195
+ baseUrl: string,
196
+ ): Record<string, ImportMapImports> | undefined {
197
+ if (!scopes) return undefined
198
+
199
+ let delta: Record<string, ImportMapImports> = {}
200
+ for (let [scope, imports] of Object.entries(scopes)) {
201
+ let normalizedScope = normalizeImportMapUrl(scope, baseUrl)
202
+ if (normalizedScope === null) continue
203
+ let installedScopeImports = installedImportMap.scopes.get(normalizedScope) ?? new Map()
204
+ let importsDelta = getImportMapImportsDelta(installedScopeImports, imports, baseUrl, scope)
205
+ if (importsDelta) delta[scope] = importsDelta
206
+ }
207
+
208
+ return Object.keys(delta).length > 0 ? delta : undefined
209
+ }
210
+
211
+ function getImportMapIntegrityDelta(
212
+ installedIntegrity: Map<string, string>,
213
+ integrity: Record<string, string> | undefined,
214
+ baseUrl: string,
215
+ ): Record<string, string> | undefined {
216
+ if (!integrity) return undefined
217
+
218
+ let delta: Record<string, string> = {}
219
+ for (let [url, metadata] of Object.entries(integrity)) {
220
+ let normalizedUrl = normalizeImportMapUrl(url, baseUrl)
221
+ if (normalizedUrl === null) continue
222
+ let installedMetadata = installedIntegrity.get(normalizedUrl)
223
+ if (installedMetadata === metadata) continue
224
+ if (installedMetadata !== undefined) {
225
+ throw new ImportMapConflictError(
226
+ `[remix] Reloading page after import map integrity conflict for "${url}": ` +
227
+ `"${installedMetadata}" is already installed, but the new map points to "${metadata}"`,
228
+ )
229
+ }
230
+ delta[url] = metadata
231
+ }
232
+
233
+ return Object.keys(delta).length > 0 ? delta : undefined
234
+ }
235
+
236
+ function mergeInstalledImportMap(
237
+ installedImportMap: InstalledImportMap,
238
+ importMap: ImportMap,
239
+ baseUrl: string,
240
+ ): void {
241
+ mergeInstalledImports(installedImportMap.imports, importMap.imports, baseUrl)
242
+
243
+ if (importMap.scopes) {
244
+ for (let [scope, imports] of Object.entries(importMap.scopes)) {
245
+ let normalizedScope = normalizeImportMapUrl(scope, baseUrl)
246
+ if (normalizedScope === null) continue
247
+ let installedScopeImports = installedImportMap.scopes.get(normalizedScope)
248
+ if (!installedScopeImports) {
249
+ installedScopeImports = new Map()
250
+ installedImportMap.scopes.set(normalizedScope, installedScopeImports)
251
+ }
252
+
253
+ mergeInstalledImports(installedScopeImports, imports, baseUrl)
254
+ }
255
+ }
256
+
257
+ if (importMap.integrity) {
258
+ for (let [url, metadata] of Object.entries(importMap.integrity)) {
259
+ let normalizedUrl = normalizeImportMapUrl(url, baseUrl)
260
+ if (normalizedUrl === null || installedImportMap.integrity.has(normalizedUrl)) continue
261
+ installedImportMap.integrity.set(normalizedUrl, metadata)
262
+ }
263
+ }
264
+ }
265
+
266
+ function mergeInstalledImports(
267
+ installed: Map<string, InstalledImportMapEntry>,
268
+ imports: ImportMapImports | undefined,
269
+ baseUrl: string,
270
+ ): void {
271
+ for (let [specifier, href] of Object.entries(imports ?? {})) {
272
+ let normalizedSpecifier = normalizeImportMapSpecifier(specifier, baseUrl)
273
+ if (normalizedSpecifier === null || installed.has(normalizedSpecifier)) continue
274
+ installed.set(normalizedSpecifier, {
275
+ href,
276
+ normalizedHref: normalizeImportMapAddress(href, baseUrl),
277
+ })
278
+ }
279
+ }
280
+
281
+ function appendImportMapScript(
282
+ doc: Document,
283
+ importMap: ImportMap,
284
+ nonce: string | undefined,
285
+ ): HTMLScriptElement {
286
+ let script = doc.createElement('script')
287
+ script.setAttribute('data-rmx-import-map', '')
288
+ script.type = 'importmap'
289
+ if (nonce) script.nonce = nonce
290
+ script.textContent = JSON.stringify(importMap)
291
+ doc.head.appendChild(script)
292
+ return script
293
+ }
294
+
295
+ function parseImportMap(json: string): ImportMap | null {
296
+ let parsed: unknown
297
+ try {
298
+ parsed = JSON.parse(json)
299
+ } catch {
300
+ return null
301
+ }
302
+
303
+ if (!parsed || typeof parsed !== 'object') return null
304
+ let importMap: ImportMap = {}
305
+
306
+ if ('imports' in parsed) {
307
+ let imports = parsed.imports
308
+ if (!isImportMapImports(imports)) return null
309
+ importMap.imports = imports
310
+ }
311
+
312
+ if ('scopes' in parsed) {
313
+ let scopes = parsed.scopes
314
+ if (!isScopedImportMapRecord(scopes)) return null
315
+ importMap.scopes = scopes
316
+ }
317
+
318
+ if ('integrity' in parsed) {
319
+ let integrity = parsed.integrity
320
+ if (!isImportMapIntegrity(integrity)) return null
321
+ importMap.integrity = integrity
322
+ }
323
+
324
+ return importMap
325
+ }
326
+
327
+ function isImportMapImports(value: unknown): value is ImportMapImports {
328
+ if (!value || typeof value !== 'object') return false
329
+ return Object.values(value).every((entry) => entry === null || typeof entry === 'string')
330
+ }
331
+
332
+ function isScopedImportMapRecord(value: unknown): value is Record<string, ImportMapImports> {
333
+ if (!value || typeof value !== 'object') return false
334
+ return Object.values(value).every(isImportMapImports)
335
+ }
336
+
337
+ function isImportMapIntegrity(value: unknown): value is Record<string, string> {
338
+ if (!value || typeof value !== 'object') return false
339
+ return Object.values(value).every((entry) => typeof entry === 'string')
340
+ }
341
+
342
+ function normalizeImportMapSpecifier(specifier: string, baseUrl: string): string | null {
343
+ if (
344
+ specifier.startsWith('/') ||
345
+ specifier.startsWith('./') ||
346
+ specifier.startsWith('../') ||
347
+ URL.canParse(specifier)
348
+ ) {
349
+ return normalizeImportMapUrl(specifier, baseUrl)
350
+ }
351
+ return specifier
352
+ }
353
+
354
+ function normalizeImportMapAddress(address: ImportMapAddress, baseUrl: string): ImportMapAddress {
355
+ if (address === null) return null
356
+ return normalizeImportMapUrl(address, baseUrl)
357
+ }
358
+
359
+ function normalizeImportMapUrl(value: string, baseUrl: string): string | null {
360
+ try {
361
+ return new URL(value, baseUrl).href
362
+ } catch {
363
+ return null
364
+ }
365
+ }
366
+
367
+ function formatImportMapAddress(address: ImportMapAddress | undefined): string {
368
+ return address === null ? 'null' : `"${address}"`
369
+ }
@@ -34,10 +34,10 @@ export const link: MixinFactory<
34
34
  return renderMixinElement(handle.element, {
35
35
  ...(props ?? {}),
36
36
  href,
37
- ...(options?.target == null ? {} : { 'rmx-target': options.target }),
38
- ...(options?.src == null ? {} : { 'rmx-src': options.src }),
39
- ...(options?.history == null ? {} : { 'rmx-history': options.history }),
40
- ...(options?.resetScroll === false ? { 'rmx-reset-scroll': 'false' } : {}),
37
+ ...(options?.target == null ? {} : { 'data-rmx-target': options.target }),
38
+ ...(options?.src == null ? {} : { 'data-rmx-src': options.src }),
39
+ ...(options?.history == null ? {} : { 'data-rmx-history': options.history }),
40
+ ...(options?.resetScroll === false ? { 'data-rmx-reset-scroll': 'false' } : {}),
41
41
  })
42
42
  }
43
43
 
@@ -6,6 +6,7 @@ import type { SchedulerPhaseEvent } from '../scheduler.ts'
6
6
  import { jsx } from '../jsx.ts'
7
7
  import { TypedEventTarget } from '../typed-event-target.ts'
8
8
  import { invariant } from '../invariant.ts'
9
+ import { composeMixedProps, isMixinElementFunction } from '../core/mix.ts'
9
10
 
10
11
  type RebindNode<value, baseNode, boundNode> = value extends (
11
12
  ...args: infer fnArgs
@@ -107,6 +108,15 @@ type MixinRuntimeType<
107
108
  type: string,
108
109
  ) => ((...args: [...args, currentProps: props]) => MixinReturn<node, props>) | void
109
110
 
111
+ type MixinDescriptorType<
112
+ args extends unknown[] = [],
113
+ node extends EventTarget = Element,
114
+ props extends ElementProps = ElementProps,
115
+ > = <boundNode extends node>(
116
+ handle: MixinHandle<boundNode, props>,
117
+ type: string,
118
+ ) => ((...args: [...args, currentProps: props]) => MixinReturn<boundNode, props>) | void
119
+
110
120
  /**
111
121
  * Public mixin setup function signature.
112
122
  */
@@ -127,7 +137,7 @@ export type MixinDescriptor<
127
137
  args extends unknown[] = [],
128
138
  props extends ElementProps = ElementProps,
129
139
  > = {
130
- type: MixinRuntimeType<args, node, props>
140
+ type: MixinDescriptorType<args, node, props>
131
141
  args: args
132
142
  readonly __node?: (node: node) => void
133
143
  }
@@ -149,13 +159,22 @@ type NestedMixValue<descriptor, depth extends number = 4> = depth extends 0
149
159
  | NullableMixValue<descriptor>
150
160
  | ReadonlyArray<NestedMixValue<descriptor, PreviousMixDepth[depth]>>
151
161
 
162
+ type MixinInputDescriptor<
163
+ in node extends EventTarget = Element,
164
+ props extends ElementProps = ElementProps,
165
+ > = {
166
+ type: (handle: MixinHandle<node, props>, type: string) => unknown
167
+ args: readonly unknown[]
168
+ readonly __node?: (node: node) => void
169
+ }
170
+
152
171
  /**
153
172
  * Accepted authoring shape for the `mix` prop on host elements.
154
173
  */
155
174
  export type MixInput<
156
175
  node extends EventTarget = Element,
157
176
  props extends ElementProps = ElementProps,
158
- > = NestedMixValue<MixinDescriptor<node, any, props>>
177
+ > = NestedMixValue<MixinInputDescriptor<node, props>>
159
178
 
160
179
  /**
161
180
  * Accepted value shape for the `mix` prop.
@@ -163,7 +182,7 @@ export type MixInput<
163
182
  export type MixValue<
164
183
  node extends EventTarget = Element,
165
184
  props extends ElementProps = ElementProps,
166
- > = MixinDescriptor<node, any, props> | ReadonlyArray<MixinDescriptor<node, any, props>>
185
+ > = MixinInputDescriptor<node, props> | ReadonlyArray<MixinInputDescriptor<node, props>>
167
186
 
168
187
  type MixinReturn<node extends EventTarget = Element, props extends ElementProps = ElementProps> =
169
188
  | void
@@ -259,16 +278,20 @@ export function createMixin<
259
278
  return <boundNode extends node = node>(
260
279
  ...args: RebindTuple<args, node, boundNode>
261
280
  ): MixinDescriptor<boundNode, RebindTuple<args, node, boundNode>, props> => ({
262
- type: type as unknown as MixinRuntimeType<RebindTuple<args, node, boundNode>, boundNode, props>,
281
+ type: type as unknown as MixinDescriptorType<
282
+ RebindTuple<args, node, boundNode>,
283
+ boundNode,
284
+ props
285
+ >,
263
286
  args: args as RebindTuple<args, node, boundNode>,
264
287
  })
265
288
  }
266
289
 
267
290
  export function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPropsOutput {
268
291
  let state = input.state ?? createMixinRuntimeState()
269
- let handle = state.handle as ScopedAnyMixinHandle | undefined
270
- if (!handle) {
271
- handle = createMixinHandle({
292
+ let scopedHandle = state.handle as ScopedAnyMixinHandle | undefined
293
+ if (!scopedHandle) {
294
+ scopedHandle = createMixinHandle({
272
295
  id: state.id,
273
296
  hostType: input.hostType,
274
297
  frame: input.frame,
@@ -277,16 +300,14 @@ export function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPr
277
300
  getRuntimeSignal: () => getMixinRuntimeSignal(state),
278
301
  getBinding: () => state.binding,
279
302
  }) as ScopedAnyMixinHandle
280
- state.handle = handle
303
+ state.handle = scopedHandle
281
304
  }
305
+ let handle = scopedHandle
282
306
  let hostType = input.hostType
283
- let descriptors = resolveMixDescriptors(input.props)
284
- let composedProps = withoutMix(input.props)
285
- let mixinProps = withoutMixinTreeProps(composedProps)
286
- let maxDescriptors = 1024
287
307
 
288
- for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {
289
- let descriptor = descriptors[index]
308
+ let runnerCount = 0
309
+ let props = composeMixedProps(hostType, input.props, (descriptor, index, mixinProps) => {
310
+ runnerCount = index + 1
290
311
  let entry = state.runners[index]
291
312
  if (!entry || entry.type !== descriptor.type) {
292
313
  if (entry) {
@@ -310,46 +331,15 @@ export function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPr
310
331
  }
311
332
  }
312
333
 
334
+ // Unlike the server renderer, mixin errors are not isolated here: a
335
+ // throwing mixin aborts the client render.
313
336
  handle.setActiveScope(entry.scope)
314
337
  let result = entry.runner(...descriptor.args, mixinProps)
315
338
  handle.setActiveScope(undefined)
316
- if (!result) continue
317
- if (isMixinElement(result)) continue
318
-
319
- let returnedDescriptors = resolveReturnedMixDescriptors(result)
320
- if (returnedDescriptors) {
321
- for (let returned of returnedDescriptors) descriptors.push(returned)
322
- continue
323
- }
324
-
325
- if (!isRemixElement(result)) {
326
- console.error(new Error('mixins must return a remix element'))
327
- continue
328
- }
329
-
330
- let resultType =
331
- typeof result.type === 'string'
332
- ? result.type
333
- : isMixinElement(result.type)
334
- ? result.type.__rmxMixinElementType
335
- : null
336
- if (resultType !== hostType) {
337
- console.error(new Error('mixins must return an element with the same host type'))
338
- continue
339
- }
340
-
341
- if (result.type !== resultType) {
342
- result = { ...result, type: resultType }
343
- }
344
-
345
- let nextProps = sanitizeReturnedMixinProps(result.props)
346
- let nestedDescriptors = resolveMixDescriptors(nextProps)
347
- for (let nested of nestedDescriptors) descriptors.push(nested)
348
- composedProps = composeMixinProps(composedProps, withoutMix(nextProps))
349
- mixinProps = withoutMixinTreeProps(composedProps)
350
- }
339
+ return result
340
+ })
351
341
 
352
- for (let index = descriptors.length; index < state.runners.length; index++) {
342
+ for (let index = runnerCount; index < state.runners.length; index++) {
353
343
  let entry = state.runners[index]
354
344
  if (entry) {
355
345
  handle.dispatchScopedEvent(entry.scope, new Event('remove'))
@@ -357,18 +347,11 @@ export function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPr
357
347
  }
358
348
  }
359
349
 
360
- if (state.runners.length > descriptors.length) {
361
- state.runners.length = descriptors.length
350
+ if (state.runners.length > runnerCount) {
351
+ state.runners.length = runnerCount
362
352
  }
363
353
 
364
- let nextMix = input.props.mix
365
- return {
366
- state,
367
- props: {
368
- ...composedProps,
369
- ...(nextMix === undefined ? {} : { mix: nextMix }),
370
- },
371
- }
354
+ return { state, props }
372
355
  }
373
356
 
374
357
  export function teardownMixins(state?: MixinRuntimeState) {
@@ -825,96 +808,8 @@ function isBindingInUpdateScope(binding: MixinRuntimeBinding, parents: ParentNod
825
808
  return false
826
809
  }
827
810
 
828
- function resolveMixDescriptors(props: ElementProps): AnyMixinDescriptor[] {
829
- let mix = props.mix
830
- if (!mix) return []
831
- if (Array.isArray(mix)) {
832
- if (mix.length === 0) return []
833
- return mix.filter(Boolean) as AnyMixinDescriptor[]
834
- }
835
- return [mix] as AnyMixinDescriptor[]
836
- }
837
-
838
- function withoutMix(props: ElementProps): ElementProps {
839
- if (!('mix' in props)) return props
840
- let output = { ...props }
841
- delete output.mix
842
- return output
843
- }
844
-
845
- function withoutMixinTreeProps(props: ElementProps): ElementProps {
846
- if (!('children' in props) && !('innerHTML' in props)) return props
847
- let output = { ...props }
848
- delete output.children
849
- delete output.innerHTML
850
- return output
851
- }
852
-
853
- function sanitizeReturnedMixinProps(props: ElementProps): ElementProps {
854
- if (!('children' in props) && !('innerHTML' in props)) return props
855
- console.error(new Error('mixins must not return children or innerHTML'))
856
- return withoutMixinTreeProps(props)
857
- }
858
-
859
- function composeMixinProps(previous: ElementProps, next: ElementProps): ElementProps {
860
- return { ...previous, ...next }
861
- }
862
-
863
- function resolveReturnedMixDescriptors(value: unknown): AnyMixinDescriptor[] | null {
864
- let descriptors: AnyMixinDescriptor[] = []
865
- if (!collectReturnedMixDescriptors(value, descriptors)) {
866
- return null
867
- }
868
-
869
- return descriptors
870
- }
871
-
872
- function collectReturnedMixDescriptors(
873
- value: unknown,
874
- output: AnyMixinDescriptor[],
875
- ): value is MixInput<Element, ElementProps> {
876
- if (!value) {
877
- return true
878
- }
879
-
880
- if (Array.isArray(value)) {
881
- for (let item of value) {
882
- if (!collectReturnedMixDescriptors(item, output)) {
883
- return false
884
- }
885
- }
886
- return true
887
- }
888
-
889
- if (!isMixinDescriptor(value)) {
890
- return false
891
- }
892
-
893
- output.push(value)
894
- return true
895
- }
896
-
897
- function isRemixElement(value: unknown): value is RemixElement {
898
- if (!value || typeof value !== 'object') return false
899
- return (value as { $rmx?: unknown }).$rmx === true
900
- }
901
-
902
- export function isMixinDescriptor(value: unknown): value is AnyMixinDescriptor {
903
- if (!value || typeof value !== 'object' || isRemixElement(value)) {
904
- return false
905
- }
906
-
907
- let descriptor = value as { type?: unknown; args?: unknown }
908
- return typeof descriptor.type === 'function' && Array.isArray(descriptor.args)
909
- }
910
-
911
- function isMixinElement(value: unknown): value is MixinElement<Element, ElementProps> {
912
- if (typeof value !== 'function') return false
913
- return '__rmxMixinElementType' in value
914
- }
915
-
916
811
  function normalizeMixinRunner(result: AnyMixinSetupResult, handle: AnyMixinHandle): AnyMixinRunner {
917
- if (typeof result === 'function' && !isMixinElement(result)) {
812
+ if (typeof result === 'function' && !isMixinElementFunction(result)) {
918
813
  return result as AnyMixinRunner
919
814
  }
920
815
  if (result === undefined) {
@@ -1,7 +1,7 @@
1
1
  import { createMixin, type MixinType } from './mixin.ts'
2
2
  import type { ElementProps } from '../jsx.ts'
3
3
  import type { MixinDescriptor } from './mixin.ts'
4
- import type { EnsureEvent, EventMap } from '../event-listeners.ts'
4
+ import type { EnsureEvent, EventMap } from '../event-types.ts'
5
5
 
6
6
  type SignaledListener<event extends Event> = (
7
7
  event: event,