@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/server/stream.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ComponentHandle,
|
|
3
|
+
FrameHandle,
|
|
4
|
+
Handle,
|
|
5
|
+
Key,
|
|
6
|
+
RemixNode,
|
|
7
|
+
RenderFn,
|
|
8
|
+
} from '../runtime/component.ts'
|
|
9
|
+
import type { ElementType, ElementProps, Props, RemixElement } from '../runtime/jsx.ts'
|
|
3
10
|
import type { ElementFunction } from '../runtime/element-function.ts'
|
|
4
11
|
import { Fragment, createComponent, createFrameHandle, Frame } from '../runtime/component.ts'
|
|
5
12
|
import { isEntry, type EntryComponent } from '../runtime/client-entries.ts'
|
|
@@ -11,6 +18,7 @@ import {
|
|
|
11
18
|
shouldStringifyBooleanAttribute,
|
|
12
19
|
} from '../runtime/core/attributes.ts'
|
|
13
20
|
import { appendFlushMarker, type FlushKind, stripFlushMarkers } from '../runtime/stream-protocol.ts'
|
|
21
|
+
import { composeMixedProps, resolveMixDescriptors } from '../runtime/core/mix.ts'
|
|
14
22
|
import { REMIX_UI_STYLE_LAYER } from '../style/layers.ts'
|
|
15
23
|
|
|
16
24
|
interface VNode {
|
|
@@ -79,6 +87,52 @@ interface ResolvedClientEntry {
|
|
|
79
87
|
exportName: string
|
|
80
88
|
/** Browser module hrefs to begin preloading before hydrating this entry. */
|
|
81
89
|
preloads?: readonly string[]
|
|
90
|
+
importMap?: ImportMapData
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Import map data accepted by the server renderer. */
|
|
94
|
+
export interface ImportMapData {
|
|
95
|
+
/** Top-level module specifier mappings. */
|
|
96
|
+
imports?: ImportMapImports
|
|
97
|
+
/** Module specifier mappings scoped by URL. */
|
|
98
|
+
scopes?: Record<string, ImportMapImports>
|
|
99
|
+
/** Subresource integrity metadata keyed by module URL. */
|
|
100
|
+
integrity?: Record<string, string>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type ImportMapAddress = string | null
|
|
104
|
+
type ImportMapImports = Record<string, ImportMapAddress>
|
|
105
|
+
type AuthoredImportMapEntries = Map<string, ImportMapAddress>
|
|
106
|
+
type AuthoredImportMapScope = { imports: AuthoredImportMapEntries }
|
|
107
|
+
type StaticSegment = { kind: 'static'; html: string }
|
|
108
|
+
type ManagedImportMap = {
|
|
109
|
+
attrs: string
|
|
110
|
+
segment: StaticSegment
|
|
111
|
+
value: ImportMapData
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type ImportMapProps = Omit<
|
|
115
|
+
Props<'script'>,
|
|
116
|
+
'children' | 'innerHTML' | 'integrity' | 'src' | 'type'
|
|
117
|
+
> & {
|
|
118
|
+
/** Initial import map entries to render and merge with resolved client entries. */
|
|
119
|
+
value: ImportMapData
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Renders the document import map and merges maps from server-resolved client entries.
|
|
124
|
+
*
|
|
125
|
+
* @param handle Server component handle containing the initial import map and script attributes.
|
|
126
|
+
* @returns This component is handled directly by the server renderer.
|
|
127
|
+
*/
|
|
128
|
+
export function ImportMap(handle: Handle<ImportMapProps>): RenderFn {
|
|
129
|
+
void handle
|
|
130
|
+
return () => null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface ClientEntryHeadResources {
|
|
134
|
+
modulePreloadTags: Set<string>
|
|
135
|
+
importMap?: ImportMapData
|
|
82
136
|
}
|
|
83
137
|
|
|
84
138
|
interface FrameData {
|
|
@@ -89,6 +143,7 @@ interface FrameData {
|
|
|
89
143
|
|
|
90
144
|
interface RenderContext {
|
|
91
145
|
insideSvg: boolean
|
|
146
|
+
insideHead: boolean
|
|
92
147
|
onError: (error: unknown) => void
|
|
93
148
|
parentVNode?: VNode
|
|
94
149
|
styleCache: Map<string, { selector: string; css: string }>
|
|
@@ -100,8 +155,12 @@ interface RenderContext {
|
|
|
100
155
|
pendingFrames: Array<{ frameId: string; promise: Promise<ResolvedFrameHtml> }>
|
|
101
156
|
hydrationData: Map<string, HydrationData>
|
|
102
157
|
unresolvedHydrationData: Map<string, UnresolvedHydrationData>
|
|
158
|
+
authoredImportMapImports: AuthoredImportMapEntries
|
|
159
|
+
authoredImportMapScopes: Map<string, AuthoredImportMapScope>
|
|
160
|
+
authoredImportMapIntegrity: Map<string, string>
|
|
161
|
+
managedImportMaps: ManagedImportMap[]
|
|
103
162
|
frameData: Map<string, FrameData>
|
|
104
|
-
|
|
163
|
+
clientEntryHeadResources: ClientEntryHeadResources
|
|
105
164
|
blockingFrameTails: ReadableStream<Uint8Array>[]
|
|
106
165
|
signal: AbortSignal
|
|
107
166
|
flushKind: FlushKind
|
|
@@ -120,7 +179,7 @@ interface SsrFrameState {
|
|
|
120
179
|
}
|
|
121
180
|
|
|
122
181
|
type Segment =
|
|
123
|
-
|
|
|
182
|
+
| StaticSegment
|
|
124
183
|
| { kind: 'composite'; parts: Segment[] }
|
|
125
184
|
| {
|
|
126
185
|
kind: 'frame'
|
|
@@ -133,7 +192,6 @@ const TEXTAREA_VALUE_PROPS = new Set(['value', 'defaultValue'])
|
|
|
133
192
|
const INPUT_DEFAULT_PROPS = new Set(['defaultValue', 'defaultChecked'])
|
|
134
193
|
|
|
135
194
|
const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
|
|
136
|
-
|
|
137
195
|
function stripDoctypeMarkup(html: string): string {
|
|
138
196
|
return html.replace(DOCTYPE_PATTERN, '')
|
|
139
197
|
}
|
|
@@ -203,14 +261,19 @@ export function renderToStream(
|
|
|
203
261
|
|
|
204
262
|
let context: RenderContext = {
|
|
205
263
|
insideSvg: false,
|
|
264
|
+
insideHead: false,
|
|
206
265
|
onError,
|
|
207
266
|
resolveFrame: options?.resolveFrame ?? defaultResolveFrame,
|
|
208
267
|
styleCache: new Map(),
|
|
209
268
|
pendingFrames: [],
|
|
210
269
|
hydrationData: new Map(),
|
|
211
270
|
unresolvedHydrationData: new Map(),
|
|
271
|
+
authoredImportMapImports: new Map(),
|
|
272
|
+
authoredImportMapScopes: new Map(),
|
|
273
|
+
authoredImportMapIntegrity: new Map(),
|
|
274
|
+
managedImportMaps: [],
|
|
212
275
|
frameData: new Map(),
|
|
213
|
-
modulePreloadTags: new Set(),
|
|
276
|
+
clientEntryHeadResources: { modulePreloadTags: new Set() },
|
|
214
277
|
blockingFrameTails: [],
|
|
215
278
|
signal: renderAbortController.signal,
|
|
216
279
|
flushKind: 'fragment',
|
|
@@ -240,6 +303,7 @@ export function renderToStream(
|
|
|
240
303
|
await resolveClientEntries(context, options?.resolveClientEntry)
|
|
241
304
|
if (closeIfCancelled(controller, context)) return
|
|
242
305
|
validateClientEntriesForHydration(context)
|
|
306
|
+
finalizeManagedImportMap(context)
|
|
243
307
|
let html = serializeSegment(root)
|
|
244
308
|
let finalHtml = finalizeHtml(html, context)
|
|
245
309
|
let bytes = encoder.encode(appendFlushMarker(finalHtml, context.flushKind))
|
|
@@ -385,16 +449,19 @@ async function splitFirstChunk(stream: ReadableStream<Uint8Array>): Promise<Reso
|
|
|
385
449
|
async function resolveFrameHtml(
|
|
386
450
|
input: string | ReadableStream<Uint8Array>,
|
|
387
451
|
): Promise<ResolvedFrameHtml> {
|
|
388
|
-
if (typeof input === 'string')
|
|
452
|
+
if (typeof input === 'string') {
|
|
453
|
+
let html = stripFlushMarkers(stripDoctypeMarkup(input))
|
|
454
|
+
return { html }
|
|
455
|
+
}
|
|
389
456
|
|
|
390
|
-
return
|
|
457
|
+
return splitFirstChunk(input)
|
|
391
458
|
}
|
|
392
459
|
|
|
393
460
|
function isRemixElement(node: unknown): node is RemixElement {
|
|
394
461
|
return typeof node === 'object' && node !== null && '$rmx' in node
|
|
395
462
|
}
|
|
396
463
|
|
|
397
|
-
function staticSeg(html: string):
|
|
464
|
+
function staticSeg(html: string): StaticSegment {
|
|
398
465
|
return { kind: 'static', html }
|
|
399
466
|
}
|
|
400
467
|
|
|
@@ -440,6 +507,9 @@ function buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFr
|
|
|
440
507
|
}
|
|
441
508
|
|
|
442
509
|
if (isElementFunction(type)) {
|
|
510
|
+
if (type === ImportMap) {
|
|
511
|
+
return buildImportMapSegment(props, context)
|
|
512
|
+
}
|
|
443
513
|
if (type === Frame) {
|
|
444
514
|
return buildFrameSegment(node, context, frameState)
|
|
445
515
|
}
|
|
@@ -492,16 +562,19 @@ function buildFrameSegment(
|
|
|
492
562
|
framePromise.catch(() => {})
|
|
493
563
|
context.pendingFrames.push({ frameId, promise: framePromise })
|
|
494
564
|
} else {
|
|
495
|
-
|
|
565
|
+
let framePromise = Promise.resolve(
|
|
496
566
|
context.resolveFrame(props.src, props.name, resolveFrameContext),
|
|
497
567
|
).then(async (resolved) => {
|
|
498
568
|
let { html, tail } = await resolveFrameHtml(resolved)
|
|
499
|
-
html =
|
|
569
|
+
html = hoistClientEntryResourcesFromFrameHead(html, context.clientEntryHeadResources)
|
|
500
570
|
seg.content = staticSeg(html)
|
|
501
571
|
if (tail) {
|
|
502
572
|
context.blockingFrameTails.push(tail)
|
|
503
573
|
}
|
|
504
574
|
})
|
|
575
|
+
// An earlier blocking frame may reject before this promise is awaited.
|
|
576
|
+
framePromise.catch(() => {})
|
|
577
|
+
seg.pending = framePromise
|
|
505
578
|
}
|
|
506
579
|
|
|
507
580
|
return seg
|
|
@@ -535,6 +608,19 @@ function buildElementSegment(
|
|
|
535
608
|
return staticSeg(`<${tag}${attrs}>${props.innerHTML}</${tag}>`)
|
|
536
609
|
}
|
|
537
610
|
|
|
611
|
+
if (tag === 'script') {
|
|
612
|
+
if (typeof props.children === 'string') {
|
|
613
|
+
if (context.insideHead) {
|
|
614
|
+
collectAuthoredImportMap(context, tag, processedProps, props.children)
|
|
615
|
+
}
|
|
616
|
+
return staticSeg(`<${tag}${attrs}>${escapeScriptTextContent(props.children)}</${tag}>`)
|
|
617
|
+
}
|
|
618
|
+
if (props.children != null) {
|
|
619
|
+
console.error(new Error('script elements with children must have a single string child'))
|
|
620
|
+
}
|
|
621
|
+
return staticSeg(`<${tag}${attrs}></${tag}>`)
|
|
622
|
+
}
|
|
623
|
+
|
|
538
624
|
let open = staticSeg(`<${tag}${attrs}>`)
|
|
539
625
|
// Adjust svg context for children: foreignObject switches back to HTML
|
|
540
626
|
let previousInsideSvg = context.insideSvg
|
|
@@ -552,6 +638,75 @@ function buildTextareaElementSegment(tag: string, props: any): Segment {
|
|
|
552
638
|
return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`)
|
|
553
639
|
}
|
|
554
640
|
|
|
641
|
+
function collectAuthoredImportMap(
|
|
642
|
+
context: RenderContext,
|
|
643
|
+
tag: string,
|
|
644
|
+
props: Record<string, unknown>,
|
|
645
|
+
children: unknown,
|
|
646
|
+
): void {
|
|
647
|
+
if (
|
|
648
|
+
tag !== 'script' ||
|
|
649
|
+
typeof props.type !== 'string' ||
|
|
650
|
+
props.type.toLowerCase() !== 'importmap' ||
|
|
651
|
+
(props.src !== undefined && props.src !== null && props.src !== false) ||
|
|
652
|
+
typeof children !== 'string'
|
|
653
|
+
) {
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let importMap = parseAuthoredImportMap(children)
|
|
658
|
+
if (!importMap) return
|
|
659
|
+
|
|
660
|
+
if (importMap.imports) {
|
|
661
|
+
collectAuthoredImportMapEntries(context.authoredImportMapImports, importMap.imports)
|
|
662
|
+
}
|
|
663
|
+
if (importMap.scopes) {
|
|
664
|
+
for (let [scope, imports] of Object.entries(importMap.scopes)) {
|
|
665
|
+
let authoredScope = context.authoredImportMapScopes.get(scope)
|
|
666
|
+
if (!authoredScope) {
|
|
667
|
+
authoredScope = { imports: new Map() }
|
|
668
|
+
context.authoredImportMapScopes.set(scope, authoredScope)
|
|
669
|
+
}
|
|
670
|
+
collectAuthoredImportMapEntries(authoredScope.imports, imports)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (importMap.integrity) {
|
|
674
|
+
for (let [url, integrity] of Object.entries(importMap.integrity)) {
|
|
675
|
+
if (!context.authoredImportMapIntegrity.has(url)) {
|
|
676
|
+
context.authoredImportMapIntegrity.set(url, integrity)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function collectAuthoredImportMapEntries(
|
|
683
|
+
target: AuthoredImportMapEntries,
|
|
684
|
+
source: ImportMapImports,
|
|
685
|
+
): void {
|
|
686
|
+
for (let [specifier, address] of Object.entries(source)) {
|
|
687
|
+
if (!target.has(specifier)) target.set(specifier, address)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function buildImportMapSegment(props: ElementProps, context: RenderContext): Segment {
|
|
692
|
+
if (context.flushKind !== 'document' || !context.insideHead) {
|
|
693
|
+
throw new Error('ImportMap must be rendered inside a document head')
|
|
694
|
+
}
|
|
695
|
+
if (context.managedImportMaps.length > 0) {
|
|
696
|
+
throw new Error('Only one ImportMap can be rendered per document')
|
|
697
|
+
}
|
|
698
|
+
let value = props.value
|
|
699
|
+
if (!isImportMap(value)) {
|
|
700
|
+
throw new TypeError('ImportMap value must be a valid import map')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
let { value: _value, ...scriptProps } = props
|
|
704
|
+
let attrs = renderAttributes(scriptProps, false)
|
|
705
|
+
let segment = staticSeg('')
|
|
706
|
+
context.managedImportMaps.push({ attrs, segment, value })
|
|
707
|
+
return segment
|
|
708
|
+
}
|
|
709
|
+
|
|
555
710
|
function renderInputAttributes(props: any): string {
|
|
556
711
|
let value =
|
|
557
712
|
props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value
|
|
@@ -577,8 +732,11 @@ function buildHeadElementSegment(
|
|
|
577
732
|
let attrs = renderAttributes(processedProps, false)
|
|
578
733
|
|
|
579
734
|
let open = staticSeg(`<${tag}${attrs}>`)
|
|
735
|
+
let previousInsideHead = context.insideHead
|
|
736
|
+
context.insideHead = true
|
|
580
737
|
let children =
|
|
581
738
|
props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')
|
|
739
|
+
context.insideHead = previousInsideHead
|
|
582
740
|
let close = staticSeg(`</${tag}>`)
|
|
583
741
|
|
|
584
742
|
return compositeSeg([open, children, close])
|
|
@@ -616,74 +774,25 @@ function resolveSsrMixedProps(
|
|
|
616
774
|
context: RenderContext,
|
|
617
775
|
frameState: SsrFrameState,
|
|
618
776
|
): ElementProps {
|
|
619
|
-
|
|
620
|
-
if (descriptors.length === 0) return initialProps
|
|
621
|
-
|
|
622
|
-
let composedProps = withoutSsrMix(initialProps)
|
|
623
|
-
let mixinProps = withoutSsrMixinTreeProps(composedProps)
|
|
624
|
-
let maxDescriptors = 1024
|
|
777
|
+
if (resolveMixDescriptors(initialProps).length === 0) return initialProps
|
|
625
778
|
|
|
626
|
-
|
|
627
|
-
let descriptor = descriptors[index]
|
|
779
|
+
return composeMixedProps(hostType, initialProps, (descriptor, _index, mixinProps) => {
|
|
628
780
|
let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState)
|
|
629
|
-
if (!runner)
|
|
630
|
-
|
|
631
|
-
|
|
781
|
+
if (!runner) return undefined
|
|
782
|
+
// Unlike the client runtime, a throwing mixin is isolated here so a
|
|
783
|
+
// single bad mixin cannot take down the whole stream.
|
|
632
784
|
try {
|
|
633
|
-
|
|
785
|
+
return runner(...descriptor.args, mixinProps)
|
|
634
786
|
} catch (error) {
|
|
635
787
|
console.error(error)
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (!result) continue
|
|
640
|
-
if (isSsrMixinElement(result)) continue
|
|
641
|
-
|
|
642
|
-
let returnedDescriptors = resolveReturnedSsrMixDescriptors(result)
|
|
643
|
-
if (returnedDescriptors) {
|
|
644
|
-
for (let returned of returnedDescriptors) descriptors.push(returned)
|
|
645
|
-
continue
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (!isRemixElement(result)) {
|
|
649
|
-
console.error(new Error('mixins must return a remix element'))
|
|
650
|
-
continue
|
|
651
|
-
}
|
|
652
|
-
let remixResult = result
|
|
653
|
-
|
|
654
|
-
let resultType =
|
|
655
|
-
typeof remixResult.type === 'string'
|
|
656
|
-
? remixResult.type
|
|
657
|
-
: isSsrMixinElement(remixResult.type)
|
|
658
|
-
? remixResult.type.__rmxMixinElementType
|
|
659
|
-
: null
|
|
660
|
-
|
|
661
|
-
if (resultType !== hostType) {
|
|
662
|
-
console.error(new Error('mixins must return an element with the same host type'))
|
|
663
|
-
continue
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (remixResult.type !== resultType) {
|
|
667
|
-
remixResult = { ...remixResult, type: resultType }
|
|
788
|
+
return undefined
|
|
668
789
|
}
|
|
669
|
-
|
|
670
|
-
let nextProps = sanitizeReturnedSsrMixinProps(remixResult.props as ElementProps)
|
|
671
|
-
let nestedDescriptors = resolveSsrMixDescriptors(nextProps)
|
|
672
|
-
for (let nested of nestedDescriptors) descriptors.push(nested)
|
|
673
|
-
composedProps = { ...composedProps, ...withoutSsrMix(nextProps) }
|
|
674
|
-
mixinProps = withoutSsrMixinTreeProps(composedProps)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
let nextMix = initialProps.mix
|
|
678
|
-
return {
|
|
679
|
-
...composedProps,
|
|
680
|
-
...(nextMix === undefined ? {} : { mix: nextMix }),
|
|
681
|
-
}
|
|
790
|
+
})
|
|
682
791
|
}
|
|
683
792
|
|
|
684
793
|
function resolveSsrMixinRunner(
|
|
685
794
|
hostType: string,
|
|
686
|
-
descriptor: { type?: unknown; args?: unknown[] },
|
|
795
|
+
descriptor: { type?: unknown; args?: readonly unknown[] },
|
|
687
796
|
context: RenderContext,
|
|
688
797
|
frameState: SsrFrameState,
|
|
689
798
|
): ((...args: unknown[]) => unknown) | null {
|
|
@@ -759,93 +868,10 @@ function createSsrMixinHandle(
|
|
|
759
868
|
}
|
|
760
869
|
}
|
|
761
870
|
|
|
762
|
-
function resolveSsrMixDescriptors(props: ElementProps): Array<{ type: any; args: unknown[] }> {
|
|
763
|
-
let mix = props.mix
|
|
764
|
-
if (!mix) return []
|
|
765
|
-
if (Array.isArray(mix)) {
|
|
766
|
-
if (mix.length === 0) return []
|
|
767
|
-
return mix.filter(Boolean) as Array<{ type: any; args: unknown[] }>
|
|
768
|
-
}
|
|
769
|
-
return [mix] as Array<{ type: any; args: unknown[] }>
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function withoutSsrMix(props: ElementProps): ElementProps {
|
|
773
|
-
if (!('mix' in props)) return props
|
|
774
|
-
let output = { ...props }
|
|
775
|
-
delete output.mix
|
|
776
|
-
return output
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function withoutSsrMixinTreeProps(props: ElementProps): ElementProps {
|
|
780
|
-
if (!('children' in props) && !('innerHTML' in props)) return props
|
|
781
|
-
let output = { ...props }
|
|
782
|
-
delete output.children
|
|
783
|
-
delete output.innerHTML
|
|
784
|
-
return output
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function sanitizeReturnedSsrMixinProps(props: ElementProps): ElementProps {
|
|
788
|
-
if (!('children' in props) && !('innerHTML' in props)) return props
|
|
789
|
-
console.error(new Error('mixins must not return children or innerHTML'))
|
|
790
|
-
return withoutSsrMixinTreeProps(props)
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function resolveReturnedSsrMixDescriptors(
|
|
794
|
-
value: unknown,
|
|
795
|
-
): Array<{ type: ElementFunction; args: unknown[] }> | null {
|
|
796
|
-
let descriptors: Array<{ type: ElementFunction; args: unknown[] }> = []
|
|
797
|
-
if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
|
|
798
|
-
return null
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return descriptors
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function collectReturnedSsrMixDescriptors(
|
|
805
|
-
value: unknown,
|
|
806
|
-
output: Array<{ type: ElementFunction; args: unknown[] }>,
|
|
807
|
-
): boolean {
|
|
808
|
-
if (!value) {
|
|
809
|
-
return true
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (Array.isArray(value)) {
|
|
813
|
-
for (let item of value) {
|
|
814
|
-
if (!collectReturnedSsrMixDescriptors(item, output)) {
|
|
815
|
-
return false
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return true
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (!isSsrMixinDescriptor(value)) {
|
|
822
|
-
return false
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
output.push(value)
|
|
826
|
-
return true
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function isSsrMixinElement(
|
|
830
|
-
value: unknown,
|
|
831
|
-
): value is ((...args: unknown[]) => unknown) & { __rmxMixinElementType: string } {
|
|
832
|
-
if (typeof value !== 'function') return false
|
|
833
|
-
return '__rmxMixinElementType' in value
|
|
834
|
-
}
|
|
835
|
-
|
|
836
871
|
function isElementFunction(value: unknown): value is ElementFunction {
|
|
837
872
|
return typeof value === 'function'
|
|
838
873
|
}
|
|
839
874
|
|
|
840
|
-
function isSsrMixinDescriptor(value: unknown): value is { type: ElementFunction; args: unknown[] } {
|
|
841
|
-
if (!value || typeof value !== 'object' || isRemixElement(value)) {
|
|
842
|
-
return false
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
let descriptor = value as { type?: unknown; args?: unknown }
|
|
846
|
-
return typeof descriptor.type === 'function' && Array.isArray(descriptor.args)
|
|
847
|
-
}
|
|
848
|
-
|
|
849
875
|
function buildComponentSegment(
|
|
850
876
|
type: ElementFunction,
|
|
851
877
|
props: any,
|
|
@@ -1071,6 +1097,7 @@ async function resolveClientEntries(
|
|
|
1071
1097
|
: resolveDefaultClientEntry(entryId, component)
|
|
1072
1098
|
validateResolvedClientEntry(entryId, resolvedEntry)
|
|
1073
1099
|
resolvedEntries.set(component, resolvedEntry)
|
|
1100
|
+
collectResolvedClientEntryResources(context.clientEntryHeadResources, resolvedEntry)
|
|
1074
1101
|
}
|
|
1075
1102
|
|
|
1076
1103
|
context.hydrationData.set(hydrationId, {
|
|
@@ -1078,15 +1105,23 @@ async function resolveClientEntries(
|
|
|
1078
1105
|
moduleUrl: resolvedEntry.href,
|
|
1079
1106
|
props,
|
|
1080
1107
|
})
|
|
1081
|
-
|
|
1082
|
-
for (let preload of resolvedEntry.preloads ?? []) {
|
|
1083
|
-
context.modulePreloadTags.add(createModulePreloadTag(preload))
|
|
1084
|
-
}
|
|
1085
1108
|
}
|
|
1086
1109
|
|
|
1087
1110
|
context.unresolvedHydrationData.clear()
|
|
1088
1111
|
}
|
|
1089
1112
|
|
|
1113
|
+
function collectResolvedClientEntryResources(
|
|
1114
|
+
resources: ClientEntryHeadResources,
|
|
1115
|
+
resolvedEntry: ResolvedClientEntry,
|
|
1116
|
+
): void {
|
|
1117
|
+
for (let preload of resolvedEntry.preloads ?? []) {
|
|
1118
|
+
resources.modulePreloadTags.add(createModulePreloadTag(preload))
|
|
1119
|
+
}
|
|
1120
|
+
if (resolvedEntry.importMap) {
|
|
1121
|
+
mergeImportMap(resources, resolvedEntry.importMap)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1090
1125
|
function validateResolvedClientEntry(
|
|
1091
1126
|
entryId: string,
|
|
1092
1127
|
resolvedEntry: ResolvedClientEntry,
|
|
@@ -1117,6 +1152,12 @@ function validateResolvedClientEntry(
|
|
|
1117
1152
|
}
|
|
1118
1153
|
}
|
|
1119
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
if (resolvedEntry.importMap !== undefined && !isImportMap(resolvedEntry.importMap)) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`resolveClientEntry importMap must be a valid import map. Received "${entryId}".`,
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1120
1161
|
}
|
|
1121
1162
|
|
|
1122
1163
|
function validateClientEntriesForHydration(context: RenderContext): void {
|
|
@@ -1174,6 +1215,16 @@ function escapeTemplateContent(html: string): string {
|
|
|
1174
1215
|
return html.replace(/<\/template/gi, '<\\/template')
|
|
1175
1216
|
}
|
|
1176
1217
|
|
|
1218
|
+
const SCRIPT_TAG_PATTERN = /(<\/|<)(s)(cript)/gi
|
|
1219
|
+
|
|
1220
|
+
function escapeScriptTextContent(value: string): string {
|
|
1221
|
+
return value.replace(
|
|
1222
|
+
SCRIPT_TAG_PATTERN,
|
|
1223
|
+
(_match, prefix: string, firstLetter: string, suffix: string) =>
|
|
1224
|
+
`${prefix}${firstLetter === 's' ? '\\u0073' : '\\u0053'}${suffix}`,
|
|
1225
|
+
)
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1177
1228
|
function transformAttributeName(name: string, isSvg: boolean): string {
|
|
1178
1229
|
return normalizeAttributeName(name, isSvg).attr
|
|
1179
1230
|
}
|
|
@@ -1181,31 +1232,29 @@ function transformAttributeName(name: string, isSvg: boolean): string {
|
|
|
1181
1232
|
function finalizeHtml(html: string, context: RenderContext): string {
|
|
1182
1233
|
let hasHtmlRoot = context.flushKind === 'document'
|
|
1183
1234
|
|
|
1184
|
-
let preloads = collectModulePreloadTags(context)
|
|
1235
|
+
let preloads = collectModulePreloadTags(context.clientEntryHeadResources)
|
|
1185
1236
|
let styles = collectStyleTags(context)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1237
|
+
let importMapScript = collectImportMapScript(context, context.clientEntryHeadResources)
|
|
1238
|
+
let headContent = importMapScript + preloads + styles
|
|
1239
|
+
if (hasHtmlRoot && headContent) {
|
|
1240
|
+
let headCloseIndex = html.indexOf('</head>')
|
|
1241
|
+
if (headCloseIndex !== -1) {
|
|
1242
|
+
html = html.slice(0, headCloseIndex) + headContent + html.slice(headCloseIndex)
|
|
1243
|
+
} else {
|
|
1244
|
+
let htmlOpenMatch = html.match(/<html[^>]*>/)
|
|
1245
|
+
if (htmlOpenMatch) {
|
|
1246
|
+
let insertIndex = htmlOpenMatch.index! + htmlOpenMatch[0].length
|
|
1247
|
+
html = html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex)
|
|
1194
1248
|
} else {
|
|
1195
|
-
|
|
1196
|
-
let htmlOpenMatch = html.match(/<html[^>]*>/)
|
|
1197
|
-
if (htmlOpenMatch) {
|
|
1198
|
-
let insertIndex = htmlOpenMatch.index! + htmlOpenMatch[0].length
|
|
1199
|
-
html =
|
|
1200
|
-
html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex)
|
|
1201
|
-
}
|
|
1249
|
+
html = headContent + html
|
|
1202
1250
|
}
|
|
1203
|
-
} else {
|
|
1204
|
-
// No HTML root, prepend head
|
|
1205
|
-
html = `<head>${headContent}</head>${html}`
|
|
1206
1251
|
}
|
|
1207
1252
|
}
|
|
1208
1253
|
|
|
1254
|
+
if (!hasHtmlRoot && headContent) {
|
|
1255
|
+
html = `<head>${headContent}</head>${html}`
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1209
1258
|
// Append aggregated hydration/frame data script at the end
|
|
1210
1259
|
let rmxData = buildRmxDataScript(context)
|
|
1211
1260
|
if (rmxData) {
|
|
@@ -1232,48 +1281,61 @@ function finalizeHtml(html: string, context: RenderContext): string {
|
|
|
1232
1281
|
|
|
1233
1282
|
const FRAME_HEAD_OPEN_TAG = '<head>'
|
|
1234
1283
|
const FRAME_HEAD_CLOSE_TAG = '</head>'
|
|
1235
|
-
const MARKED_MODULE_PRELOAD_START = '<link data-rmx rel="modulepreload" href="'
|
|
1284
|
+
const MARKED_MODULE_PRELOAD_START = '<link data-rmx-module-preload rel="modulepreload" href="'
|
|
1236
1285
|
const MODULE_PRELOAD_END = '" />'
|
|
1286
|
+
const MANAGED_IMPORT_MAP_START = '<script data-rmx-import-map type="importmap">'
|
|
1287
|
+
const IMPORT_MAP_SCRIPT_END = '</script>'
|
|
1237
1288
|
|
|
1238
1289
|
function createModulePreloadTag(href: string): string {
|
|
1239
1290
|
return `${MARKED_MODULE_PRELOAD_START}${escapeHtml(href)}${MODULE_PRELOAD_END}`
|
|
1240
1291
|
}
|
|
1241
1292
|
|
|
1242
|
-
function collectModulePreloadTags(
|
|
1243
|
-
return Array.from(
|
|
1293
|
+
function collectModulePreloadTags(resources: ClientEntryHeadResources): string {
|
|
1294
|
+
return Array.from(resources.modulePreloadTags).join('')
|
|
1244
1295
|
}
|
|
1245
1296
|
|
|
1246
|
-
function
|
|
1297
|
+
function hoistClientEntryResourcesFromFrameHead(
|
|
1298
|
+
html: string,
|
|
1299
|
+
resources: ClientEntryHeadResources,
|
|
1300
|
+
): string {
|
|
1247
1301
|
if (!html.startsWith(FRAME_HEAD_OPEN_TAG)) return html
|
|
1248
1302
|
|
|
1249
|
-
let
|
|
1250
|
-
|
|
1251
|
-
while (html.startsWith(MARKED_MODULE_PRELOAD_START, remainingHeadStart)) {
|
|
1252
|
-
let tagEnd = html.indexOf(
|
|
1253
|
-
MODULE_PRELOAD_END,
|
|
1254
|
-
remainingHeadStart + MARKED_MODULE_PRELOAD_START.length,
|
|
1255
|
-
)
|
|
1256
|
-
if (tagEnd === -1) return html
|
|
1303
|
+
let headClose = html.indexOf(FRAME_HEAD_CLOSE_TAG, FRAME_HEAD_OPEN_TAG.length)
|
|
1304
|
+
if (headClose === -1) return html
|
|
1257
1305
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1306
|
+
let preloadTags: string[] = []
|
|
1307
|
+
let importMaps: ImportMapData[] = []
|
|
1308
|
+
let cursor = FRAME_HEAD_OPEN_TAG.length
|
|
1309
|
+
if (html.startsWith(MANAGED_IMPORT_MAP_START, cursor)) {
|
|
1310
|
+
let contentStart = cursor + MANAGED_IMPORT_MAP_START.length
|
|
1311
|
+
let scriptEnd = html.indexOf(IMPORT_MAP_SCRIPT_END, contentStart)
|
|
1312
|
+
if (scriptEnd === -1 || scriptEnd >= headClose) return html
|
|
1313
|
+
importMaps.push(parseFrameworkImportMap(html.slice(contentStart, scriptEnd)))
|
|
1314
|
+
cursor = scriptEnd + IMPORT_MAP_SCRIPT_END.length
|
|
1261
1315
|
}
|
|
1262
1316
|
|
|
1263
|
-
|
|
1317
|
+
while (html.startsWith(MARKED_MODULE_PRELOAD_START, cursor)) {
|
|
1318
|
+
let tagEnd = html.indexOf(MODULE_PRELOAD_END, cursor + MARKED_MODULE_PRELOAD_START.length)
|
|
1319
|
+
if (tagEnd === -1 || tagEnd >= headClose) return html
|
|
1320
|
+
tagEnd += MODULE_PRELOAD_END.length
|
|
1321
|
+
preloadTags.push(html.slice(cursor, tagEnd))
|
|
1322
|
+
cursor = tagEnd
|
|
1323
|
+
}
|
|
1264
1324
|
|
|
1265
|
-
|
|
1266
|
-
if (headClose === -1) return html
|
|
1325
|
+
if (preloadTags.length === 0 && importMaps.length === 0) return html
|
|
1267
1326
|
|
|
1268
|
-
for (let tag of
|
|
1269
|
-
|
|
1327
|
+
for (let tag of preloadTags) {
|
|
1328
|
+
resources.modulePreloadTags.add(tag)
|
|
1270
1329
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
return html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length)
|
|
1330
|
+
for (let importMap of importMaps) {
|
|
1331
|
+
mergeImportMap(resources, importMap)
|
|
1274
1332
|
}
|
|
1275
1333
|
|
|
1276
|
-
|
|
1334
|
+
let remainingHeadHtml = html.slice(cursor, headClose)
|
|
1335
|
+
let contentAfterHead = html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length)
|
|
1336
|
+
if (!remainingHeadHtml) return contentAfterHead
|
|
1337
|
+
|
|
1338
|
+
return `${FRAME_HEAD_OPEN_TAG}${remainingHeadHtml}${FRAME_HEAD_CLOSE_TAG}${contentAfterHead}`
|
|
1277
1339
|
}
|
|
1278
1340
|
|
|
1279
1341
|
function processStyleProps(props: any): any {
|
|
@@ -1322,12 +1384,12 @@ function renderStyleTag(
|
|
|
1322
1384
|
): string {
|
|
1323
1385
|
let wrappedCss = wrapStyleForLayer(selector, css, layer)
|
|
1324
1386
|
if (!wrappedCss) return ''
|
|
1325
|
-
return `<style data-rmx="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`
|
|
1387
|
+
return `<style data-rmx-style="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`
|
|
1326
1388
|
}
|
|
1327
1389
|
|
|
1328
1390
|
function escapeStyleText(css: string): string {
|
|
1329
|
-
//
|
|
1330
|
-
return css.replace(
|
|
1391
|
+
// Only neutralize literal style end tags. Escaping every '<' breaks valid range media queries.
|
|
1392
|
+
return css.replace(/<\/style/gi, '\\3C/style')
|
|
1331
1393
|
}
|
|
1332
1394
|
|
|
1333
1395
|
function buildRmxDataScript(context: RenderContext): string {
|
|
@@ -1352,11 +1414,253 @@ function buildRmxDataScript(context: RenderContext): string {
|
|
|
1352
1414
|
return `<script type="application/json" id="rmx-data">${serializedData}</script>`
|
|
1353
1415
|
}
|
|
1354
1416
|
|
|
1417
|
+
function buildImportMapScript(importMap: ImportMapData, attrs: string = ''): string {
|
|
1418
|
+
let serializedData = escapeScriptJson(JSON.stringify(importMap))
|
|
1419
|
+
return `<script data-rmx-import-map type="importmap"${attrs}>${serializedData}</script>`
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function collectImportMapScript(
|
|
1423
|
+
context: RenderContext,
|
|
1424
|
+
resources: ClientEntryHeadResources,
|
|
1425
|
+
): string {
|
|
1426
|
+
let importMap = getImportMapDelta(context, resources.importMap)
|
|
1427
|
+
return importMap ? buildImportMapScript(importMap) : ''
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function finalizeManagedImportMap(context: RenderContext): void {
|
|
1431
|
+
let managed = context.managedImportMaps[0]
|
|
1432
|
+
if (!managed) return
|
|
1433
|
+
|
|
1434
|
+
let resources: ClientEntryHeadResources = { modulePreloadTags: new Set() }
|
|
1435
|
+
mergeImportMap(resources, managed.value)
|
|
1436
|
+
if (context.clientEntryHeadResources.importMap) {
|
|
1437
|
+
mergeImportMap(resources, context.clientEntryHeadResources.importMap)
|
|
1438
|
+
}
|
|
1439
|
+
managed.segment.html = buildImportMapScript(resources.importMap ?? {}, managed.attrs)
|
|
1440
|
+
context.clientEntryHeadResources.importMap = undefined
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function getImportMapDelta(
|
|
1444
|
+
context: RenderContext,
|
|
1445
|
+
importMap: ImportMapData | undefined,
|
|
1446
|
+
): ImportMapData | null {
|
|
1447
|
+
if (!importMap) return null
|
|
1448
|
+
|
|
1449
|
+
let imports = importMap.imports
|
|
1450
|
+
? getImportMapImportsDelta(context.authoredImportMapImports, importMap.imports)
|
|
1451
|
+
: undefined
|
|
1452
|
+
let scopes: Record<string, ImportMapImports> = {}
|
|
1453
|
+
for (let [scope, scopedImports] of Object.entries(importMap.scopes ?? {})) {
|
|
1454
|
+
let authoredImports =
|
|
1455
|
+
context.authoredImportMapScopes.get(scope)?.imports ?? new Map<string, ImportMapAddress>()
|
|
1456
|
+
let importsDelta = getImportMapImportsDelta(authoredImports, scopedImports, scope)
|
|
1457
|
+
if (importsDelta) scopes[scope] = importsDelta
|
|
1458
|
+
}
|
|
1459
|
+
let integrity = importMap.integrity
|
|
1460
|
+
? getImportMapIntegrityDelta(context.authoredImportMapIntegrity, importMap.integrity)
|
|
1461
|
+
: undefined
|
|
1462
|
+
|
|
1463
|
+
if (!imports && Object.keys(scopes).length === 0 && !integrity) return null
|
|
1464
|
+
return {
|
|
1465
|
+
...(imports ? { imports } : null),
|
|
1466
|
+
...(Object.keys(scopes).length > 0 ? { scopes } : null),
|
|
1467
|
+
...(integrity ? { integrity } : null),
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function getImportMapImportsDelta(
|
|
1472
|
+
authoredImports: AuthoredImportMapEntries,
|
|
1473
|
+
discoveredImports: ImportMapImports,
|
|
1474
|
+
scope?: string,
|
|
1475
|
+
): ImportMapImports | undefined {
|
|
1476
|
+
let delta: ImportMapImports = {}
|
|
1477
|
+
for (let [specifier, address] of Object.entries(discoveredImports)) {
|
|
1478
|
+
if (!authoredImports.has(specifier)) {
|
|
1479
|
+
delta[specifier] = address
|
|
1480
|
+
continue
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
let authoredAddress = authoredImports.get(specifier)
|
|
1484
|
+
if (authoredAddress === address) continue
|
|
1485
|
+
|
|
1486
|
+
let scopeDescription = scope ? ` in scope "${scope}"` : ''
|
|
1487
|
+
console.warn(
|
|
1488
|
+
`[remix] Ignoring conflicting import map entry for "${specifier}"${scopeDescription}: ` +
|
|
1489
|
+
`${formatImportMapAddress(authoredAddress)} is already authored, but the discovered map points to ${formatImportMapAddress(address)}`,
|
|
1490
|
+
)
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return Object.keys(delta).length > 0 ? delta : undefined
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function getImportMapIntegrityDelta(
|
|
1497
|
+
authoredIntegrity: Map<string, string>,
|
|
1498
|
+
discoveredIntegrity: Record<string, string>,
|
|
1499
|
+
): Record<string, string> | undefined {
|
|
1500
|
+
let delta: Record<string, string> = {}
|
|
1501
|
+
for (let [url, integrity] of Object.entries(discoveredIntegrity)) {
|
|
1502
|
+
if (!authoredIntegrity.has(url)) {
|
|
1503
|
+
delta[url] = integrity
|
|
1504
|
+
continue
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
let authoredIntegrityValue = authoredIntegrity.get(url)
|
|
1508
|
+
if (authoredIntegrityValue === integrity) continue
|
|
1509
|
+
|
|
1510
|
+
console.warn(
|
|
1511
|
+
`[remix] Ignoring conflicting import map integrity entry for "${url}": ` +
|
|
1512
|
+
`"${authoredIntegrityValue}" is already authored, but the discovered map points to "${integrity}"`,
|
|
1513
|
+
)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return Object.keys(delta).length > 0 ? delta : undefined
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function parseAuthoredImportMap(json: string): ImportMapData | null {
|
|
1520
|
+
let value: unknown
|
|
1521
|
+
try {
|
|
1522
|
+
value = JSON.parse(json)
|
|
1523
|
+
} catch {
|
|
1524
|
+
return null
|
|
1525
|
+
}
|
|
1526
|
+
if (!isObjectRecord(value)) return null
|
|
1527
|
+
|
|
1528
|
+
let importMap: ImportMapData = {}
|
|
1529
|
+
if (value.imports !== undefined) {
|
|
1530
|
+
if (!isObjectRecord(value.imports)) return null
|
|
1531
|
+
importMap.imports = parseAuthoredImportMapImports(value.imports)
|
|
1532
|
+
}
|
|
1533
|
+
if (value.scopes !== undefined) {
|
|
1534
|
+
if (!isObjectRecord(value.scopes)) return null
|
|
1535
|
+
let scopes: Array<[string, ImportMapImports]> = []
|
|
1536
|
+
for (let [scope, imports] of Object.entries(value.scopes)) {
|
|
1537
|
+
if (!isObjectRecord(imports)) continue
|
|
1538
|
+
scopes.push([scope, parseAuthoredImportMapImports(imports)])
|
|
1539
|
+
}
|
|
1540
|
+
importMap.scopes = Object.fromEntries(scopes)
|
|
1541
|
+
}
|
|
1542
|
+
if (value.integrity !== undefined) {
|
|
1543
|
+
if (!isObjectRecord(value.integrity)) return null
|
|
1544
|
+
importMap.integrity = Object.fromEntries(
|
|
1545
|
+
Object.entries(value.integrity).filter(
|
|
1546
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
1547
|
+
),
|
|
1548
|
+
)
|
|
1549
|
+
}
|
|
1550
|
+
return importMap
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function parseAuthoredImportMapImports(value: Record<string, unknown>): ImportMapImports {
|
|
1554
|
+
return Object.fromEntries(
|
|
1555
|
+
Object.entries(value).map(([specifier, address]) => [
|
|
1556
|
+
specifier,
|
|
1557
|
+
address === null || typeof address === 'string' ? address : null,
|
|
1558
|
+
]),
|
|
1559
|
+
)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function parseFrameworkImportMap(json: string): ImportMapData {
|
|
1563
|
+
let value: unknown
|
|
1564
|
+
try {
|
|
1565
|
+
value = JSON.parse(json)
|
|
1566
|
+
} catch {
|
|
1567
|
+
throw new Error('Invalid framework-owned import map in frame head')
|
|
1568
|
+
}
|
|
1569
|
+
if (!isImportMap(value)) {
|
|
1570
|
+
throw new Error('Invalid framework-owned import map in frame head')
|
|
1571
|
+
}
|
|
1572
|
+
return value
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function isImportMap(value: unknown): value is ImportMapData {
|
|
1576
|
+
if (!isObjectRecord(value)) return false
|
|
1577
|
+
if (value.imports !== undefined && !isImportMapImports(value.imports)) return false
|
|
1578
|
+
if (value.scopes !== undefined) {
|
|
1579
|
+
if (!isObjectRecord(value.scopes)) return false
|
|
1580
|
+
for (let imports of Object.values(value.scopes)) {
|
|
1581
|
+
if (!isImportMapImports(imports)) return false
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (value.integrity !== undefined && !isImportMapIntegrity(value.integrity)) return false
|
|
1585
|
+
return true
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function isImportMapImports(value: unknown): value is ImportMapImports {
|
|
1589
|
+
if (!isObjectRecord(value)) return false
|
|
1590
|
+
return Object.values(value).every((address) => address === null || typeof address === 'string')
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function isImportMapIntegrity(value: unknown): value is Record<string, string> {
|
|
1594
|
+
if (!isObjectRecord(value)) return false
|
|
1595
|
+
return Object.values(value).every((integrity) => typeof integrity === 'string')
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
1599
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function mergeImportMap(resources: ClientEntryHeadResources, source: ImportMapData): void {
|
|
1603
|
+
let target = (resources.importMap ??= {})
|
|
1604
|
+
if (source.imports) {
|
|
1605
|
+
target.imports ??= {}
|
|
1606
|
+
mergeImportMapImports(target.imports, source.imports)
|
|
1607
|
+
}
|
|
1608
|
+
if (source.scopes) {
|
|
1609
|
+
target.scopes ??= {}
|
|
1610
|
+
for (let [scope, imports] of Object.entries(source.scopes)) {
|
|
1611
|
+
let targetImports = (target.scopes[scope] ??= {})
|
|
1612
|
+
mergeImportMapImports(targetImports, imports, scope)
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (source.integrity) {
|
|
1616
|
+
target.integrity ??= {}
|
|
1617
|
+
mergeImportMapIntegrity(target.integrity, source.integrity)
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function mergeImportMapImports(
|
|
1622
|
+
target: ImportMapImports,
|
|
1623
|
+
source: ImportMapImports,
|
|
1624
|
+
scope?: string,
|
|
1625
|
+
): void {
|
|
1626
|
+
for (let [specifier, address] of Object.entries(source)) {
|
|
1627
|
+
if (!Object.hasOwn(target, specifier)) {
|
|
1628
|
+
target[specifier] = address
|
|
1629
|
+
continue
|
|
1630
|
+
}
|
|
1631
|
+
if (target[specifier] !== address) {
|
|
1632
|
+
let scopeDescription = scope ? ` in scope "${scope}"` : ''
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
`Conflicting framework import map entry for "${specifier}"${scopeDescription}`,
|
|
1635
|
+
)
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function mergeImportMapIntegrity(
|
|
1641
|
+
target: Record<string, string>,
|
|
1642
|
+
source: Record<string, string>,
|
|
1643
|
+
): void {
|
|
1644
|
+
for (let [url, integrity] of Object.entries(source)) {
|
|
1645
|
+
if (!Object.hasOwn(target, url)) {
|
|
1646
|
+
target[url] = integrity
|
|
1647
|
+
continue
|
|
1648
|
+
}
|
|
1649
|
+
if (target[url] !== integrity) {
|
|
1650
|
+
throw new Error(`Conflicting framework import map integrity entry for "${url}"`)
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1355
1655
|
function escapeScriptJson(json: string): string {
|
|
1356
1656
|
// Avoid prematurely closing the script tag when serialized data contains "</script>".
|
|
1357
1657
|
return json.replace(/</g, '\\u003c')
|
|
1358
1658
|
}
|
|
1359
1659
|
|
|
1660
|
+
function formatImportMapAddress(address: ImportMapAddress | undefined): string {
|
|
1661
|
+
return address === null ? 'null' : `"${address}"`
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1360
1664
|
// Frame styles work end-to-end when frame handlers use their own `renderToStream`:
|
|
1361
1665
|
// the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
|
|
1362
1666
|
// the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
|