@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
|
@@ -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:
|
|
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<
|
|
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
|
-
> =
|
|
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
|
|
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
|
|
270
|
-
if (!
|
|
271
|
-
|
|
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 =
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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 =
|
|
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 >
|
|
361
|
-
state.runners.length =
|
|
350
|
+
if (state.runners.length > runnerCount) {
|
|
351
|
+
state.runners.length = runnerCount
|
|
362
352
|
}
|
|
363
353
|
|
|
364
|
-
|
|
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' && !
|
|
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-
|
|
4
|
+
import type { EnsureEvent, EventMap } from '../event-types.ts'
|
|
5
5
|
|
|
6
6
|
type SignaledListener<event extends Event> = (
|
|
7
7
|
event: event,
|