@remix-run/ui 0.8.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 +56 -8
- 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 +7 -13
- 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/frame.d.ts +19 -4
- package/dist/runtime/frame.js +80 -11
- 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/mixin.d.ts +0 -1
- package/dist/runtime/mixins/mixin.js +18 -132
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/module-preloader.d.ts +2 -1
- package/dist/runtime/module-preloader.js +3 -2
- package/dist/runtime/module-preloader.js.map +1 -1
- package/dist/runtime/navigation.js +143 -48
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +9 -0
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +3 -0
- package/dist/runtime/run.js +3 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/to-vnode.js +1 -1
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/server/stream.d.ts +25 -2
- package/dist/server/stream.js +361 -166
- package/dist/server/stream.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/component.ts +52 -14
- package/src/runtime/core/mix.ts +157 -0
- package/src/runtime/diff-dom.ts +7 -12
- package/src/runtime/document-reload.ts +14 -0
- package/src/runtime/frame.ts +105 -16
- package/src/runtime/import-map-manager.ts +369 -0
- package/src/runtime/mixins/mixin.ts +18 -145
- package/src/runtime/module-preloader.ts +6 -3
- package/src/runtime/navigation.ts +178 -58
- package/src/runtime/reconcile.ts +9 -0
- package/src/runtime/run.ts +7 -1
- package/src/runtime/to-vnode.ts +1 -1
- package/src/server/README.md +25 -5
- package/src/server/stream.ts +478 -197
- package/src/test/utils.ts +1 -6
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
|
}
|
|
@@ -496,7 +566,7 @@ function buildFrameSegment(
|
|
|
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)
|
|
@@ -540,6 +610,9 @@ function buildElementSegment(
|
|
|
540
610
|
|
|
541
611
|
if (tag === 'script') {
|
|
542
612
|
if (typeof props.children === 'string') {
|
|
613
|
+
if (context.insideHead) {
|
|
614
|
+
collectAuthoredImportMap(context, tag, processedProps, props.children)
|
|
615
|
+
}
|
|
543
616
|
return staticSeg(`<${tag}${attrs}>${escapeScriptTextContent(props.children)}</${tag}>`)
|
|
544
617
|
}
|
|
545
618
|
if (props.children != null) {
|
|
@@ -565,6 +638,75 @@ function buildTextareaElementSegment(tag: string, props: any): Segment {
|
|
|
565
638
|
return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`)
|
|
566
639
|
}
|
|
567
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
|
+
|
|
568
710
|
function renderInputAttributes(props: any): string {
|
|
569
711
|
let value =
|
|
570
712
|
props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value
|
|
@@ -590,8 +732,11 @@ function buildHeadElementSegment(
|
|
|
590
732
|
let attrs = renderAttributes(processedProps, false)
|
|
591
733
|
|
|
592
734
|
let open = staticSeg(`<${tag}${attrs}>`)
|
|
735
|
+
let previousInsideHead = context.insideHead
|
|
736
|
+
context.insideHead = true
|
|
593
737
|
let children =
|
|
594
738
|
props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')
|
|
739
|
+
context.insideHead = previousInsideHead
|
|
595
740
|
let close = staticSeg(`</${tag}>`)
|
|
596
741
|
|
|
597
742
|
return compositeSeg([open, children, close])
|
|
@@ -629,74 +774,25 @@ function resolveSsrMixedProps(
|
|
|
629
774
|
context: RenderContext,
|
|
630
775
|
frameState: SsrFrameState,
|
|
631
776
|
): ElementProps {
|
|
632
|
-
|
|
633
|
-
if (descriptors.length === 0) return initialProps
|
|
634
|
-
|
|
635
|
-
let composedProps = withoutSsrMix(initialProps)
|
|
636
|
-
let mixinProps = withoutSsrMixinTreeProps(composedProps)
|
|
637
|
-
let maxDescriptors = 1024
|
|
777
|
+
if (resolveMixDescriptors(initialProps).length === 0) return initialProps
|
|
638
778
|
|
|
639
|
-
|
|
640
|
-
let descriptor = descriptors[index]
|
|
779
|
+
return composeMixedProps(hostType, initialProps, (descriptor, _index, mixinProps) => {
|
|
641
780
|
let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState)
|
|
642
|
-
if (!runner)
|
|
643
|
-
|
|
644
|
-
|
|
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.
|
|
645
784
|
try {
|
|
646
|
-
|
|
785
|
+
return runner(...descriptor.args, mixinProps)
|
|
647
786
|
} catch (error) {
|
|
648
787
|
console.error(error)
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (!result) continue
|
|
653
|
-
if (isSsrMixinElement(result)) continue
|
|
654
|
-
|
|
655
|
-
let returnedDescriptors = resolveReturnedSsrMixDescriptors(result)
|
|
656
|
-
if (returnedDescriptors) {
|
|
657
|
-
for (let returned of returnedDescriptors) descriptors.push(returned)
|
|
658
|
-
continue
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (!isRemixElement(result)) {
|
|
662
|
-
console.error(new Error('mixins must return a remix element'))
|
|
663
|
-
continue
|
|
664
|
-
}
|
|
665
|
-
let remixResult = result
|
|
666
|
-
|
|
667
|
-
let resultType =
|
|
668
|
-
typeof remixResult.type === 'string'
|
|
669
|
-
? remixResult.type
|
|
670
|
-
: isSsrMixinElement(remixResult.type)
|
|
671
|
-
? remixResult.type.__rmxMixinElementType
|
|
672
|
-
: null
|
|
673
|
-
|
|
674
|
-
if (resultType !== hostType) {
|
|
675
|
-
console.error(new Error('mixins must return an element with the same host type'))
|
|
676
|
-
continue
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (remixResult.type !== resultType) {
|
|
680
|
-
remixResult = { ...remixResult, type: resultType }
|
|
788
|
+
return undefined
|
|
681
789
|
}
|
|
682
|
-
|
|
683
|
-
let nextProps = sanitizeReturnedSsrMixinProps(remixResult.props as ElementProps)
|
|
684
|
-
let nestedDescriptors = resolveSsrMixDescriptors(nextProps)
|
|
685
|
-
for (let nested of nestedDescriptors) descriptors.push(nested)
|
|
686
|
-
composedProps = { ...composedProps, ...withoutSsrMix(nextProps) }
|
|
687
|
-
mixinProps = withoutSsrMixinTreeProps(composedProps)
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
let nextMix = initialProps.mix
|
|
691
|
-
return {
|
|
692
|
-
...composedProps,
|
|
693
|
-
...(nextMix === undefined ? {} : { mix: nextMix }),
|
|
694
|
-
}
|
|
790
|
+
})
|
|
695
791
|
}
|
|
696
792
|
|
|
697
793
|
function resolveSsrMixinRunner(
|
|
698
794
|
hostType: string,
|
|
699
|
-
descriptor: { type?: unknown; args?: unknown[] },
|
|
795
|
+
descriptor: { type?: unknown; args?: readonly unknown[] },
|
|
700
796
|
context: RenderContext,
|
|
701
797
|
frameState: SsrFrameState,
|
|
702
798
|
): ((...args: unknown[]) => unknown) | null {
|
|
@@ -772,93 +868,10 @@ function createSsrMixinHandle(
|
|
|
772
868
|
}
|
|
773
869
|
}
|
|
774
870
|
|
|
775
|
-
function resolveSsrMixDescriptors(props: ElementProps): Array<{ type: any; args: unknown[] }> {
|
|
776
|
-
let mix = props.mix
|
|
777
|
-
if (!mix) return []
|
|
778
|
-
if (Array.isArray(mix)) {
|
|
779
|
-
if (mix.length === 0) return []
|
|
780
|
-
return mix.filter(Boolean) as Array<{ type: any; args: unknown[] }>
|
|
781
|
-
}
|
|
782
|
-
return [mix] as Array<{ type: any; args: unknown[] }>
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function withoutSsrMix(props: ElementProps): ElementProps {
|
|
786
|
-
if (!('mix' in props)) return props
|
|
787
|
-
let output = { ...props }
|
|
788
|
-
delete output.mix
|
|
789
|
-
return output
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function withoutSsrMixinTreeProps(props: ElementProps): ElementProps {
|
|
793
|
-
if (!('children' in props) && !('innerHTML' in props)) return props
|
|
794
|
-
let output = { ...props }
|
|
795
|
-
delete output.children
|
|
796
|
-
delete output.innerHTML
|
|
797
|
-
return output
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function sanitizeReturnedSsrMixinProps(props: ElementProps): ElementProps {
|
|
801
|
-
if (!('children' in props) && !('innerHTML' in props)) return props
|
|
802
|
-
console.error(new Error('mixins must not return children or innerHTML'))
|
|
803
|
-
return withoutSsrMixinTreeProps(props)
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function resolveReturnedSsrMixDescriptors(
|
|
807
|
-
value: unknown,
|
|
808
|
-
): Array<{ type: ElementFunction; args: unknown[] }> | null {
|
|
809
|
-
let descriptors: Array<{ type: ElementFunction; args: unknown[] }> = []
|
|
810
|
-
if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
|
|
811
|
-
return null
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
return descriptors
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function collectReturnedSsrMixDescriptors(
|
|
818
|
-
value: unknown,
|
|
819
|
-
output: Array<{ type: ElementFunction; args: unknown[] }>,
|
|
820
|
-
): boolean {
|
|
821
|
-
if (!value) {
|
|
822
|
-
return true
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (Array.isArray(value)) {
|
|
826
|
-
for (let item of value) {
|
|
827
|
-
if (!collectReturnedSsrMixDescriptors(item, output)) {
|
|
828
|
-
return false
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return true
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (!isSsrMixinDescriptor(value)) {
|
|
835
|
-
return false
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
output.push(value)
|
|
839
|
-
return true
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function isSsrMixinElement(
|
|
843
|
-
value: unknown,
|
|
844
|
-
): value is ((...args: unknown[]) => unknown) & { __rmxMixinElementType: string } {
|
|
845
|
-
if (typeof value !== 'function') return false
|
|
846
|
-
return '__rmxMixinElementType' in value
|
|
847
|
-
}
|
|
848
|
-
|
|
849
871
|
function isElementFunction(value: unknown): value is ElementFunction {
|
|
850
872
|
return typeof value === 'function'
|
|
851
873
|
}
|
|
852
874
|
|
|
853
|
-
function isSsrMixinDescriptor(value: unknown): value is { type: ElementFunction; args: unknown[] } {
|
|
854
|
-
if (!value || typeof value !== 'object' || isRemixElement(value)) {
|
|
855
|
-
return false
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
let descriptor = value as { type?: unknown; args?: unknown }
|
|
859
|
-
return typeof descriptor.type === 'function' && Array.isArray(descriptor.args)
|
|
860
|
-
}
|
|
861
|
-
|
|
862
875
|
function buildComponentSegment(
|
|
863
876
|
type: ElementFunction,
|
|
864
877
|
props: any,
|
|
@@ -1084,6 +1097,7 @@ async function resolveClientEntries(
|
|
|
1084
1097
|
: resolveDefaultClientEntry(entryId, component)
|
|
1085
1098
|
validateResolvedClientEntry(entryId, resolvedEntry)
|
|
1086
1099
|
resolvedEntries.set(component, resolvedEntry)
|
|
1100
|
+
collectResolvedClientEntryResources(context.clientEntryHeadResources, resolvedEntry)
|
|
1087
1101
|
}
|
|
1088
1102
|
|
|
1089
1103
|
context.hydrationData.set(hydrationId, {
|
|
@@ -1091,15 +1105,23 @@ async function resolveClientEntries(
|
|
|
1091
1105
|
moduleUrl: resolvedEntry.href,
|
|
1092
1106
|
props,
|
|
1093
1107
|
})
|
|
1094
|
-
|
|
1095
|
-
for (let preload of resolvedEntry.preloads ?? []) {
|
|
1096
|
-
context.modulePreloadTags.add(createModulePreloadTag(preload))
|
|
1097
|
-
}
|
|
1098
1108
|
}
|
|
1099
1109
|
|
|
1100
1110
|
context.unresolvedHydrationData.clear()
|
|
1101
1111
|
}
|
|
1102
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
|
+
|
|
1103
1125
|
function validateResolvedClientEntry(
|
|
1104
1126
|
entryId: string,
|
|
1105
1127
|
resolvedEntry: ResolvedClientEntry,
|
|
@@ -1130,6 +1152,12 @@ function validateResolvedClientEntry(
|
|
|
1130
1152
|
}
|
|
1131
1153
|
}
|
|
1132
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
|
+
}
|
|
1133
1161
|
}
|
|
1134
1162
|
|
|
1135
1163
|
function validateClientEntriesForHydration(context: RenderContext): void {
|
|
@@ -1204,31 +1232,29 @@ function transformAttributeName(name: string, isSvg: boolean): string {
|
|
|
1204
1232
|
function finalizeHtml(html: string, context: RenderContext): string {
|
|
1205
1233
|
let hasHtmlRoot = context.flushKind === 'document'
|
|
1206
1234
|
|
|
1207
|
-
let preloads = collectModulePreloadTags(context)
|
|
1235
|
+
let preloads = collectModulePreloadTags(context.clientEntryHeadResources)
|
|
1208
1236
|
let styles = collectStyleTags(context)
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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)
|
|
1217
1248
|
} else {
|
|
1218
|
-
|
|
1219
|
-
let htmlOpenMatch = html.match(/<html[^>]*>/)
|
|
1220
|
-
if (htmlOpenMatch) {
|
|
1221
|
-
let insertIndex = htmlOpenMatch.index! + htmlOpenMatch[0].length
|
|
1222
|
-
html =
|
|
1223
|
-
html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex)
|
|
1224
|
-
}
|
|
1249
|
+
html = headContent + html
|
|
1225
1250
|
}
|
|
1226
|
-
} else {
|
|
1227
|
-
// No HTML root, prepend head
|
|
1228
|
-
html = `<head>${headContent}</head>${html}`
|
|
1229
1251
|
}
|
|
1230
1252
|
}
|
|
1231
1253
|
|
|
1254
|
+
if (!hasHtmlRoot && headContent) {
|
|
1255
|
+
html = `<head>${headContent}</head>${html}`
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1232
1258
|
// Append aggregated hydration/frame data script at the end
|
|
1233
1259
|
let rmxData = buildRmxDataScript(context)
|
|
1234
1260
|
if (rmxData) {
|
|
@@ -1257,46 +1283,59 @@ const FRAME_HEAD_OPEN_TAG = '<head>'
|
|
|
1257
1283
|
const FRAME_HEAD_CLOSE_TAG = '</head>'
|
|
1258
1284
|
const MARKED_MODULE_PRELOAD_START = '<link data-rmx-module-preload rel="modulepreload" href="'
|
|
1259
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>'
|
|
1260
1288
|
|
|
1261
1289
|
function createModulePreloadTag(href: string): string {
|
|
1262
1290
|
return `${MARKED_MODULE_PRELOAD_START}${escapeHtml(href)}${MODULE_PRELOAD_END}`
|
|
1263
1291
|
}
|
|
1264
1292
|
|
|
1265
|
-
function collectModulePreloadTags(
|
|
1266
|
-
return Array.from(
|
|
1293
|
+
function collectModulePreloadTags(resources: ClientEntryHeadResources): string {
|
|
1294
|
+
return Array.from(resources.modulePreloadTags).join('')
|
|
1267
1295
|
}
|
|
1268
1296
|
|
|
1269
|
-
function
|
|
1297
|
+
function hoistClientEntryResourcesFromFrameHead(
|
|
1298
|
+
html: string,
|
|
1299
|
+
resources: ClientEntryHeadResources,
|
|
1300
|
+
): string {
|
|
1270
1301
|
if (!html.startsWith(FRAME_HEAD_OPEN_TAG)) return html
|
|
1271
1302
|
|
|
1272
|
-
let
|
|
1273
|
-
|
|
1274
|
-
while (html.startsWith(MARKED_MODULE_PRELOAD_START, remainingHeadStart)) {
|
|
1275
|
-
let tagEnd = html.indexOf(
|
|
1276
|
-
MODULE_PRELOAD_END,
|
|
1277
|
-
remainingHeadStart + MARKED_MODULE_PRELOAD_START.length,
|
|
1278
|
-
)
|
|
1279
|
-
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
|
|
1280
1305
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
|
1284
1315
|
}
|
|
1285
1316
|
|
|
1286
|
-
|
|
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
|
+
}
|
|
1287
1324
|
|
|
1288
|
-
|
|
1289
|
-
if (headClose === -1) return html
|
|
1325
|
+
if (preloadTags.length === 0 && importMaps.length === 0) return html
|
|
1290
1326
|
|
|
1291
|
-
for (let tag of
|
|
1292
|
-
|
|
1327
|
+
for (let tag of preloadTags) {
|
|
1328
|
+
resources.modulePreloadTags.add(tag)
|
|
1293
1329
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
return html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length)
|
|
1330
|
+
for (let importMap of importMaps) {
|
|
1331
|
+
mergeImportMap(resources, importMap)
|
|
1297
1332
|
}
|
|
1298
1333
|
|
|
1299
|
-
|
|
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}`
|
|
1300
1339
|
}
|
|
1301
1340
|
|
|
1302
1341
|
function processStyleProps(props: any): any {
|
|
@@ -1375,11 +1414,253 @@ function buildRmxDataScript(context: RenderContext): string {
|
|
|
1375
1414
|
return `<script type="application/json" id="rmx-data">${serializedData}</script>`
|
|
1376
1415
|
}
|
|
1377
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
|
+
|
|
1378
1655
|
function escapeScriptJson(json: string): string {
|
|
1379
1656
|
// Avoid prematurely closing the script tag when serialized data contains "</script>".
|
|
1380
1657
|
return json.replace(/</g, '\\u003c')
|
|
1381
1658
|
}
|
|
1382
1659
|
|
|
1660
|
+
function formatImportMapAddress(address: ImportMapAddress | undefined): string {
|
|
1661
|
+
return address === null ? 'null' : `"${address}"`
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1383
1664
|
// Frame styles work end-to-end when frame handlers use their own `renderToStream`:
|
|
1384
1665
|
// the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
|
|
1385
1666
|
// the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
|