@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/dist/server/stream.js
CHANGED
|
@@ -2,10 +2,21 @@ import { Fragment, createComponent, createFrameHandle, Frame } from '../runtime/
|
|
|
2
2
|
import { isEntry } from '../runtime/client-entries.js';
|
|
3
3
|
import { FRAMEWORK_PROPS as RUNTIME_FRAMEWORK_PROPS, SELF_CLOSING_TAGS, normalizeAttributeName, serializeStyleObject, shouldStringifyBooleanAttribute, } from '../runtime/core/attributes.js';
|
|
4
4
|
import { appendFlushMarker, stripFlushMarkers } from '../runtime/stream-protocol.js';
|
|
5
|
+
import { composeMixedProps, resolveMixDescriptors } from '../runtime/core/mix.js';
|
|
5
6
|
import { REMIX_UI_STYLE_LAYER } from '../style/layers.js';
|
|
6
7
|
export function createVNode(type, props, key) {
|
|
7
8
|
return { type, props, key };
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Renders the document import map and merges maps from server-resolved client entries.
|
|
12
|
+
*
|
|
13
|
+
* @param handle Server component handle containing the initial import map and script attributes.
|
|
14
|
+
* @returns This component is handled directly by the server renderer.
|
|
15
|
+
*/
|
|
16
|
+
export function ImportMap(handle) {
|
|
17
|
+
void handle;
|
|
18
|
+
return () => null;
|
|
19
|
+
}
|
|
9
20
|
const TEXTAREA_VALUE_PROPS = new Set(['value', 'defaultValue']);
|
|
10
21
|
const INPUT_DEFAULT_PROPS = new Set(['defaultValue', 'defaultChecked']);
|
|
11
22
|
const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi;
|
|
@@ -60,14 +71,19 @@ export function renderToStream(node, options) {
|
|
|
60
71
|
let renderAbortController = new AbortController();
|
|
61
72
|
let context = {
|
|
62
73
|
insideSvg: false,
|
|
74
|
+
insideHead: false,
|
|
63
75
|
onError,
|
|
64
76
|
resolveFrame: options?.resolveFrame ?? defaultResolveFrame,
|
|
65
77
|
styleCache: new Map(),
|
|
66
78
|
pendingFrames: [],
|
|
67
79
|
hydrationData: new Map(),
|
|
68
80
|
unresolvedHydrationData: new Map(),
|
|
81
|
+
authoredImportMapImports: new Map(),
|
|
82
|
+
authoredImportMapScopes: new Map(),
|
|
83
|
+
authoredImportMapIntegrity: new Map(),
|
|
84
|
+
managedImportMaps: [],
|
|
69
85
|
frameData: new Map(),
|
|
70
|
-
modulePreloadTags: new Set(),
|
|
86
|
+
clientEntryHeadResources: { modulePreloadTags: new Set() },
|
|
71
87
|
blockingFrameTails: [],
|
|
72
88
|
signal: renderAbortController.signal,
|
|
73
89
|
flushKind: 'fragment',
|
|
@@ -97,6 +113,7 @@ export function renderToStream(node, options) {
|
|
|
97
113
|
if (closeIfCancelled(controller, context))
|
|
98
114
|
return;
|
|
99
115
|
validateClientEntriesForHydration(context);
|
|
116
|
+
finalizeManagedImportMap(context);
|
|
100
117
|
let html = serializeSegment(root);
|
|
101
118
|
let finalHtml = finalizeHtml(html, context);
|
|
102
119
|
let bytes = encoder.encode(appendFlushMarker(finalHtml, context.flushKind));
|
|
@@ -222,9 +239,11 @@ async function splitFirstChunk(stream) {
|
|
|
222
239
|
return { html: stripFlushMarkers(stripDoctypeMarkup(decoder.decode(first))), tail };
|
|
223
240
|
}
|
|
224
241
|
async function resolveFrameHtml(input) {
|
|
225
|
-
if (typeof input === 'string')
|
|
226
|
-
|
|
227
|
-
|
|
242
|
+
if (typeof input === 'string') {
|
|
243
|
+
let html = stripFlushMarkers(stripDoctypeMarkup(input));
|
|
244
|
+
return { html };
|
|
245
|
+
}
|
|
246
|
+
return splitFirstChunk(input);
|
|
228
247
|
}
|
|
229
248
|
function isRemixElement(node) {
|
|
230
249
|
return typeof node === 'object' && node !== null && '$rmx' in node;
|
|
@@ -264,6 +283,9 @@ function buildSegment(node, context, frameState) {
|
|
|
264
283
|
return buildElementSegment(tag, props, context, frameState);
|
|
265
284
|
}
|
|
266
285
|
if (isElementFunction(type)) {
|
|
286
|
+
if (type === ImportMap) {
|
|
287
|
+
return buildImportMapSegment(props, context);
|
|
288
|
+
}
|
|
267
289
|
if (type === Frame) {
|
|
268
290
|
return buildFrameSegment(node, context, frameState);
|
|
269
291
|
}
|
|
@@ -302,7 +324,7 @@ function buildFrameSegment(node, context, frameState) {
|
|
|
302
324
|
else {
|
|
303
325
|
let framePromise = Promise.resolve(context.resolveFrame(props.src, props.name, resolveFrameContext)).then(async (resolved) => {
|
|
304
326
|
let { html, tail } = await resolveFrameHtml(resolved);
|
|
305
|
-
html =
|
|
327
|
+
html = hoistClientEntryResourcesFromFrameHead(html, context.clientEntryHeadResources);
|
|
306
328
|
seg.content = staticSeg(html);
|
|
307
329
|
if (tail) {
|
|
308
330
|
context.blockingFrameTails.push(tail);
|
|
@@ -333,6 +355,9 @@ function buildElementSegment(tag, props, context, frameState) {
|
|
|
333
355
|
}
|
|
334
356
|
if (tag === 'script') {
|
|
335
357
|
if (typeof props.children === 'string') {
|
|
358
|
+
if (context.insideHead) {
|
|
359
|
+
collectAuthoredImportMap(context, tag, processedProps, props.children);
|
|
360
|
+
}
|
|
336
361
|
return staticSeg(`<${tag}${attrs}>${escapeScriptTextContent(props.children)}</${tag}>`);
|
|
337
362
|
}
|
|
338
363
|
if (props.children != null) {
|
|
@@ -354,6 +379,61 @@ function buildTextareaElementSegment(tag, props) {
|
|
|
354
379
|
let value = props.value ?? props.defaultValue ?? '';
|
|
355
380
|
return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`);
|
|
356
381
|
}
|
|
382
|
+
function collectAuthoredImportMap(context, tag, props, children) {
|
|
383
|
+
if (tag !== 'script' ||
|
|
384
|
+
typeof props.type !== 'string' ||
|
|
385
|
+
props.type.toLowerCase() !== 'importmap' ||
|
|
386
|
+
(props.src !== undefined && props.src !== null && props.src !== false) ||
|
|
387
|
+
typeof children !== 'string') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
let importMap = parseAuthoredImportMap(children);
|
|
391
|
+
if (!importMap)
|
|
392
|
+
return;
|
|
393
|
+
if (importMap.imports) {
|
|
394
|
+
collectAuthoredImportMapEntries(context.authoredImportMapImports, importMap.imports);
|
|
395
|
+
}
|
|
396
|
+
if (importMap.scopes) {
|
|
397
|
+
for (let [scope, imports] of Object.entries(importMap.scopes)) {
|
|
398
|
+
let authoredScope = context.authoredImportMapScopes.get(scope);
|
|
399
|
+
if (!authoredScope) {
|
|
400
|
+
authoredScope = { imports: new Map() };
|
|
401
|
+
context.authoredImportMapScopes.set(scope, authoredScope);
|
|
402
|
+
}
|
|
403
|
+
collectAuthoredImportMapEntries(authoredScope.imports, imports);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (importMap.integrity) {
|
|
407
|
+
for (let [url, integrity] of Object.entries(importMap.integrity)) {
|
|
408
|
+
if (!context.authoredImportMapIntegrity.has(url)) {
|
|
409
|
+
context.authoredImportMapIntegrity.set(url, integrity);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function collectAuthoredImportMapEntries(target, source) {
|
|
415
|
+
for (let [specifier, address] of Object.entries(source)) {
|
|
416
|
+
if (!target.has(specifier))
|
|
417
|
+
target.set(specifier, address);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function buildImportMapSegment(props, context) {
|
|
421
|
+
if (context.flushKind !== 'document' || !context.insideHead) {
|
|
422
|
+
throw new Error('ImportMap must be rendered inside a document head');
|
|
423
|
+
}
|
|
424
|
+
if (context.managedImportMaps.length > 0) {
|
|
425
|
+
throw new Error('Only one ImportMap can be rendered per document');
|
|
426
|
+
}
|
|
427
|
+
let value = props.value;
|
|
428
|
+
if (!isImportMap(value)) {
|
|
429
|
+
throw new TypeError('ImportMap value must be a valid import map');
|
|
430
|
+
}
|
|
431
|
+
let { value: _value, ...scriptProps } = props;
|
|
432
|
+
let attrs = renderAttributes(scriptProps, false);
|
|
433
|
+
let segment = staticSeg('');
|
|
434
|
+
context.managedImportMaps.push({ attrs, segment, value });
|
|
435
|
+
return segment;
|
|
436
|
+
}
|
|
357
437
|
function renderInputAttributes(props) {
|
|
358
438
|
let value = props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value;
|
|
359
439
|
let checked = props.checked === undefined && props.defaultChecked !== undefined
|
|
@@ -370,7 +450,10 @@ function buildHeadElementSegment(tag, props, context, frameState) {
|
|
|
370
450
|
let processedProps = processStyleProps(props);
|
|
371
451
|
let attrs = renderAttributes(processedProps, false);
|
|
372
452
|
let open = staticSeg(`<${tag}${attrs}>`);
|
|
453
|
+
let previousInsideHead = context.insideHead;
|
|
454
|
+
context.insideHead = true;
|
|
373
455
|
let children = props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('');
|
|
456
|
+
context.insideHead = previousInsideHead;
|
|
374
457
|
let close = staticSeg(`</${tag}>`);
|
|
375
458
|
return compositeSeg([open, children, close]);
|
|
376
459
|
}
|
|
@@ -400,64 +483,22 @@ function renderAttributes(props, isSvg, excludedProps) {
|
|
|
400
483
|
return attrs;
|
|
401
484
|
}
|
|
402
485
|
function resolveSsrMixedProps(hostType, initialProps, context, frameState) {
|
|
403
|
-
|
|
404
|
-
if (descriptors.length === 0)
|
|
486
|
+
if (resolveMixDescriptors(initialProps).length === 0)
|
|
405
487
|
return initialProps;
|
|
406
|
-
|
|
407
|
-
let mixinProps = withoutSsrMixinTreeProps(composedProps);
|
|
408
|
-
let maxDescriptors = 1024;
|
|
409
|
-
for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {
|
|
410
|
-
let descriptor = descriptors[index];
|
|
488
|
+
return composeMixedProps(hostType, initialProps, (descriptor, _index, mixinProps) => {
|
|
411
489
|
let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState);
|
|
412
490
|
if (!runner)
|
|
413
|
-
|
|
414
|
-
|
|
491
|
+
return undefined;
|
|
492
|
+
// Unlike the client runtime, a throwing mixin is isolated here so a
|
|
493
|
+
// single bad mixin cannot take down the whole stream.
|
|
415
494
|
try {
|
|
416
|
-
|
|
495
|
+
return runner(...descriptor.args, mixinProps);
|
|
417
496
|
}
|
|
418
497
|
catch (error) {
|
|
419
498
|
console.error(error);
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
if (!result)
|
|
423
|
-
continue;
|
|
424
|
-
if (isSsrMixinElement(result))
|
|
425
|
-
continue;
|
|
426
|
-
let returnedDescriptors = resolveReturnedSsrMixDescriptors(result);
|
|
427
|
-
if (returnedDescriptors) {
|
|
428
|
-
for (let returned of returnedDescriptors)
|
|
429
|
-
descriptors.push(returned);
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (!isRemixElement(result)) {
|
|
433
|
-
console.error(new Error('mixins must return a remix element'));
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
let remixResult = result;
|
|
437
|
-
let resultType = typeof remixResult.type === 'string'
|
|
438
|
-
? remixResult.type
|
|
439
|
-
: isSsrMixinElement(remixResult.type)
|
|
440
|
-
? remixResult.type.__rmxMixinElementType
|
|
441
|
-
: null;
|
|
442
|
-
if (resultType !== hostType) {
|
|
443
|
-
console.error(new Error('mixins must return an element with the same host type'));
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
if (remixResult.type !== resultType) {
|
|
447
|
-
remixResult = { ...remixResult, type: resultType };
|
|
499
|
+
return undefined;
|
|
448
500
|
}
|
|
449
|
-
|
|
450
|
-
let nestedDescriptors = resolveSsrMixDescriptors(nextProps);
|
|
451
|
-
for (let nested of nestedDescriptors)
|
|
452
|
-
descriptors.push(nested);
|
|
453
|
-
composedProps = { ...composedProps, ...withoutSsrMix(nextProps) };
|
|
454
|
-
mixinProps = withoutSsrMixinTreeProps(composedProps);
|
|
455
|
-
}
|
|
456
|
-
let nextMix = initialProps.mix;
|
|
457
|
-
return {
|
|
458
|
-
...composedProps,
|
|
459
|
-
...(nextMix === undefined ? {} : { mix: nextMix }),
|
|
460
|
-
};
|
|
501
|
+
});
|
|
461
502
|
}
|
|
462
503
|
function resolveSsrMixinRunner(hostType, descriptor, context, frameState) {
|
|
463
504
|
if (typeof descriptor.type !== 'function')
|
|
@@ -520,78 +561,9 @@ function createSsrMixinHandle(hostType, _descriptor, context, frameState) {
|
|
|
520
561
|
dispatchEvent: () => true,
|
|
521
562
|
};
|
|
522
563
|
}
|
|
523
|
-
function resolveSsrMixDescriptors(props) {
|
|
524
|
-
let mix = props.mix;
|
|
525
|
-
if (!mix)
|
|
526
|
-
return [];
|
|
527
|
-
if (Array.isArray(mix)) {
|
|
528
|
-
if (mix.length === 0)
|
|
529
|
-
return [];
|
|
530
|
-
return mix.filter(Boolean);
|
|
531
|
-
}
|
|
532
|
-
return [mix];
|
|
533
|
-
}
|
|
534
|
-
function withoutSsrMix(props) {
|
|
535
|
-
if (!('mix' in props))
|
|
536
|
-
return props;
|
|
537
|
-
let output = { ...props };
|
|
538
|
-
delete output.mix;
|
|
539
|
-
return output;
|
|
540
|
-
}
|
|
541
|
-
function withoutSsrMixinTreeProps(props) {
|
|
542
|
-
if (!('children' in props) && !('innerHTML' in props))
|
|
543
|
-
return props;
|
|
544
|
-
let output = { ...props };
|
|
545
|
-
delete output.children;
|
|
546
|
-
delete output.innerHTML;
|
|
547
|
-
return output;
|
|
548
|
-
}
|
|
549
|
-
function sanitizeReturnedSsrMixinProps(props) {
|
|
550
|
-
if (!('children' in props) && !('innerHTML' in props))
|
|
551
|
-
return props;
|
|
552
|
-
console.error(new Error('mixins must not return children or innerHTML'));
|
|
553
|
-
return withoutSsrMixinTreeProps(props);
|
|
554
|
-
}
|
|
555
|
-
function resolveReturnedSsrMixDescriptors(value) {
|
|
556
|
-
let descriptors = [];
|
|
557
|
-
if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
return descriptors;
|
|
561
|
-
}
|
|
562
|
-
function collectReturnedSsrMixDescriptors(value, output) {
|
|
563
|
-
if (!value) {
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
if (Array.isArray(value)) {
|
|
567
|
-
for (let item of value) {
|
|
568
|
-
if (!collectReturnedSsrMixDescriptors(item, output)) {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
if (!isSsrMixinDescriptor(value)) {
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
output.push(value);
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
function isSsrMixinElement(value) {
|
|
581
|
-
if (typeof value !== 'function')
|
|
582
|
-
return false;
|
|
583
|
-
return '__rmxMixinElementType' in value;
|
|
584
|
-
}
|
|
585
564
|
function isElementFunction(value) {
|
|
586
565
|
return typeof value === 'function';
|
|
587
566
|
}
|
|
588
|
-
function isSsrMixinDescriptor(value) {
|
|
589
|
-
if (!value || typeof value !== 'object' || isRemixElement(value)) {
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
592
|
-
let descriptor = value;
|
|
593
|
-
return typeof descriptor.type === 'function' && Array.isArray(descriptor.args);
|
|
594
|
-
}
|
|
595
567
|
function buildComponentSegment(type, props, context, componentId, frameState) {
|
|
596
568
|
let vnode = createVNode(type, props);
|
|
597
569
|
if (context.parentVNode) {
|
|
@@ -778,18 +750,24 @@ async function resolveClientEntries(context, resolveClientEntry) {
|
|
|
778
750
|
: resolveDefaultClientEntry(entryId, component);
|
|
779
751
|
validateResolvedClientEntry(entryId, resolvedEntry);
|
|
780
752
|
resolvedEntries.set(component, resolvedEntry);
|
|
753
|
+
collectResolvedClientEntryResources(context.clientEntryHeadResources, resolvedEntry);
|
|
781
754
|
}
|
|
782
755
|
context.hydrationData.set(hydrationId, {
|
|
783
756
|
exportName: resolvedEntry.exportName,
|
|
784
757
|
moduleUrl: resolvedEntry.href,
|
|
785
758
|
props,
|
|
786
759
|
});
|
|
787
|
-
for (let preload of resolvedEntry.preloads ?? []) {
|
|
788
|
-
context.modulePreloadTags.add(createModulePreloadTag(preload));
|
|
789
|
-
}
|
|
790
760
|
}
|
|
791
761
|
context.unresolvedHydrationData.clear();
|
|
792
762
|
}
|
|
763
|
+
function collectResolvedClientEntryResources(resources, resolvedEntry) {
|
|
764
|
+
for (let preload of resolvedEntry.preloads ?? []) {
|
|
765
|
+
resources.modulePreloadTags.add(createModulePreloadTag(preload));
|
|
766
|
+
}
|
|
767
|
+
if (resolvedEntry.importMap) {
|
|
768
|
+
mergeImportMap(resources, resolvedEntry.importMap);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
793
771
|
function validateResolvedClientEntry(entryId, resolvedEntry) {
|
|
794
772
|
if (!resolvedEntry || typeof resolvedEntry !== 'object') {
|
|
795
773
|
throw new Error(`resolveClientEntry must return an object with href and exportName. Received "${entryId}".`);
|
|
@@ -810,6 +788,9 @@ function validateResolvedClientEntry(entryId, resolvedEntry) {
|
|
|
810
788
|
}
|
|
811
789
|
}
|
|
812
790
|
}
|
|
791
|
+
if (resolvedEntry.importMap !== undefined && !isImportMap(resolvedEntry.importMap)) {
|
|
792
|
+
throw new Error(`resolveClientEntry importMap must be a valid import map. Received "${entryId}".`);
|
|
793
|
+
}
|
|
813
794
|
}
|
|
814
795
|
function validateClientEntriesForHydration(context) {
|
|
815
796
|
if (context.unresolvedHydrationData.size > 0) {
|
|
@@ -870,31 +851,28 @@ function transformAttributeName(name, isSvg) {
|
|
|
870
851
|
}
|
|
871
852
|
function finalizeHtml(html, context) {
|
|
872
853
|
let hasHtmlRoot = context.flushKind === 'document';
|
|
873
|
-
let preloads = collectModulePreloadTags(context);
|
|
854
|
+
let preloads = collectModulePreloadTags(context.clientEntryHeadResources);
|
|
874
855
|
let styles = collectStyleTags(context);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
856
|
+
let importMapScript = collectImportMapScript(context, context.clientEntryHeadResources);
|
|
857
|
+
let headContent = importMapScript + preloads + styles;
|
|
858
|
+
if (hasHtmlRoot && headContent) {
|
|
859
|
+
let headCloseIndex = html.indexOf('</head>');
|
|
860
|
+
if (headCloseIndex !== -1) {
|
|
861
|
+
html = html.slice(0, headCloseIndex) + headContent + html.slice(headCloseIndex);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
let htmlOpenMatch = html.match(/<html[^>]*>/);
|
|
865
|
+
if (htmlOpenMatch) {
|
|
866
|
+
let insertIndex = htmlOpenMatch.index + htmlOpenMatch[0].length;
|
|
867
|
+
html = html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex);
|
|
883
868
|
}
|
|
884
869
|
else {
|
|
885
|
-
|
|
886
|
-
let htmlOpenMatch = html.match(/<html[^>]*>/);
|
|
887
|
-
if (htmlOpenMatch) {
|
|
888
|
-
let insertIndex = htmlOpenMatch.index + htmlOpenMatch[0].length;
|
|
889
|
-
html =
|
|
890
|
-
html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex);
|
|
891
|
-
}
|
|
870
|
+
html = headContent + html;
|
|
892
871
|
}
|
|
893
872
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
873
|
+
}
|
|
874
|
+
if (!hasHtmlRoot && headContent) {
|
|
875
|
+
html = `<head>${headContent}</head>${html}`;
|
|
898
876
|
}
|
|
899
877
|
// Append aggregated hydration/frame data script at the end
|
|
900
878
|
let rmxData = buildRmxDataScript(context);
|
|
@@ -925,37 +903,52 @@ const FRAME_HEAD_OPEN_TAG = '<head>';
|
|
|
925
903
|
const FRAME_HEAD_CLOSE_TAG = '</head>';
|
|
926
904
|
const MARKED_MODULE_PRELOAD_START = '<link data-rmx-module-preload rel="modulepreload" href="';
|
|
927
905
|
const MODULE_PRELOAD_END = '" />';
|
|
906
|
+
const MANAGED_IMPORT_MAP_START = '<script data-rmx-import-map type="importmap">';
|
|
907
|
+
const IMPORT_MAP_SCRIPT_END = '</script>';
|
|
928
908
|
function createModulePreloadTag(href) {
|
|
929
909
|
return `${MARKED_MODULE_PRELOAD_START}${escapeHtml(href)}${MODULE_PRELOAD_END}`;
|
|
930
910
|
}
|
|
931
|
-
function collectModulePreloadTags(
|
|
932
|
-
return Array.from(
|
|
911
|
+
function collectModulePreloadTags(resources) {
|
|
912
|
+
return Array.from(resources.modulePreloadTags).join('');
|
|
933
913
|
}
|
|
934
|
-
function
|
|
914
|
+
function hoistClientEntryResourcesFromFrameHead(html, resources) {
|
|
935
915
|
if (!html.startsWith(FRAME_HEAD_OPEN_TAG))
|
|
936
916
|
return html;
|
|
937
|
-
let
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
917
|
+
let headClose = html.indexOf(FRAME_HEAD_CLOSE_TAG, FRAME_HEAD_OPEN_TAG.length);
|
|
918
|
+
if (headClose === -1)
|
|
919
|
+
return html;
|
|
920
|
+
let preloadTags = [];
|
|
921
|
+
let importMaps = [];
|
|
922
|
+
let cursor = FRAME_HEAD_OPEN_TAG.length;
|
|
923
|
+
if (html.startsWith(MANAGED_IMPORT_MAP_START, cursor)) {
|
|
924
|
+
let contentStart = cursor + MANAGED_IMPORT_MAP_START.length;
|
|
925
|
+
let scriptEnd = html.indexOf(IMPORT_MAP_SCRIPT_END, contentStart);
|
|
926
|
+
if (scriptEnd === -1 || scriptEnd >= headClose)
|
|
927
|
+
return html;
|
|
928
|
+
importMaps.push(parseFrameworkImportMap(html.slice(contentStart, scriptEnd)));
|
|
929
|
+
cursor = scriptEnd + IMPORT_MAP_SCRIPT_END.length;
|
|
930
|
+
}
|
|
931
|
+
while (html.startsWith(MARKED_MODULE_PRELOAD_START, cursor)) {
|
|
932
|
+
let tagEnd = html.indexOf(MODULE_PRELOAD_END, cursor + MARKED_MODULE_PRELOAD_START.length);
|
|
933
|
+
if (tagEnd === -1 || tagEnd >= headClose)
|
|
942
934
|
return html;
|
|
943
935
|
tagEnd += MODULE_PRELOAD_END.length;
|
|
944
|
-
|
|
945
|
-
|
|
936
|
+
preloadTags.push(html.slice(cursor, tagEnd));
|
|
937
|
+
cursor = tagEnd;
|
|
946
938
|
}
|
|
947
|
-
if (
|
|
948
|
-
return html;
|
|
949
|
-
let headClose = html.indexOf(FRAME_HEAD_CLOSE_TAG, remainingHeadStart);
|
|
950
|
-
if (headClose === -1)
|
|
939
|
+
if (preloadTags.length === 0 && importMaps.length === 0)
|
|
951
940
|
return html;
|
|
952
|
-
for (let tag of
|
|
953
|
-
|
|
941
|
+
for (let tag of preloadTags) {
|
|
942
|
+
resources.modulePreloadTags.add(tag);
|
|
954
943
|
}
|
|
955
|
-
|
|
956
|
-
|
|
944
|
+
for (let importMap of importMaps) {
|
|
945
|
+
mergeImportMap(resources, importMap);
|
|
957
946
|
}
|
|
958
|
-
|
|
947
|
+
let remainingHeadHtml = html.slice(cursor, headClose);
|
|
948
|
+
let contentAfterHead = html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length);
|
|
949
|
+
if (!remainingHeadHtml)
|
|
950
|
+
return contentAfterHead;
|
|
951
|
+
return `${FRAME_HEAD_OPEN_TAG}${remainingHeadHtml}${FRAME_HEAD_CLOSE_TAG}${contentAfterHead}`;
|
|
959
952
|
}
|
|
960
953
|
function processStyleProps(props) {
|
|
961
954
|
let processedProps = { ...props };
|
|
@@ -1012,10 +1005,212 @@ function buildRmxDataScript(context) {
|
|
|
1012
1005
|
let serializedData = escapeScriptJson(JSON.stringify(data));
|
|
1013
1006
|
return `<script type="application/json" id="rmx-data">${serializedData}</script>`;
|
|
1014
1007
|
}
|
|
1008
|
+
function buildImportMapScript(importMap, attrs = '') {
|
|
1009
|
+
let serializedData = escapeScriptJson(JSON.stringify(importMap));
|
|
1010
|
+
return `<script data-rmx-import-map type="importmap"${attrs}>${serializedData}</script>`;
|
|
1011
|
+
}
|
|
1012
|
+
function collectImportMapScript(context, resources) {
|
|
1013
|
+
let importMap = getImportMapDelta(context, resources.importMap);
|
|
1014
|
+
return importMap ? buildImportMapScript(importMap) : '';
|
|
1015
|
+
}
|
|
1016
|
+
function finalizeManagedImportMap(context) {
|
|
1017
|
+
let managed = context.managedImportMaps[0];
|
|
1018
|
+
if (!managed)
|
|
1019
|
+
return;
|
|
1020
|
+
let resources = { modulePreloadTags: new Set() };
|
|
1021
|
+
mergeImportMap(resources, managed.value);
|
|
1022
|
+
if (context.clientEntryHeadResources.importMap) {
|
|
1023
|
+
mergeImportMap(resources, context.clientEntryHeadResources.importMap);
|
|
1024
|
+
}
|
|
1025
|
+
managed.segment.html = buildImportMapScript(resources.importMap ?? {}, managed.attrs);
|
|
1026
|
+
context.clientEntryHeadResources.importMap = undefined;
|
|
1027
|
+
}
|
|
1028
|
+
function getImportMapDelta(context, importMap) {
|
|
1029
|
+
if (!importMap)
|
|
1030
|
+
return null;
|
|
1031
|
+
let imports = importMap.imports
|
|
1032
|
+
? getImportMapImportsDelta(context.authoredImportMapImports, importMap.imports)
|
|
1033
|
+
: undefined;
|
|
1034
|
+
let scopes = {};
|
|
1035
|
+
for (let [scope, scopedImports] of Object.entries(importMap.scopes ?? {})) {
|
|
1036
|
+
let authoredImports = context.authoredImportMapScopes.get(scope)?.imports ?? new Map();
|
|
1037
|
+
let importsDelta = getImportMapImportsDelta(authoredImports, scopedImports, scope);
|
|
1038
|
+
if (importsDelta)
|
|
1039
|
+
scopes[scope] = importsDelta;
|
|
1040
|
+
}
|
|
1041
|
+
let integrity = importMap.integrity
|
|
1042
|
+
? getImportMapIntegrityDelta(context.authoredImportMapIntegrity, importMap.integrity)
|
|
1043
|
+
: undefined;
|
|
1044
|
+
if (!imports && Object.keys(scopes).length === 0 && !integrity)
|
|
1045
|
+
return null;
|
|
1046
|
+
return {
|
|
1047
|
+
...(imports ? { imports } : null),
|
|
1048
|
+
...(Object.keys(scopes).length > 0 ? { scopes } : null),
|
|
1049
|
+
...(integrity ? { integrity } : null),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function getImportMapImportsDelta(authoredImports, discoveredImports, scope) {
|
|
1053
|
+
let delta = {};
|
|
1054
|
+
for (let [specifier, address] of Object.entries(discoveredImports)) {
|
|
1055
|
+
if (!authoredImports.has(specifier)) {
|
|
1056
|
+
delta[specifier] = address;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
let authoredAddress = authoredImports.get(specifier);
|
|
1060
|
+
if (authoredAddress === address)
|
|
1061
|
+
continue;
|
|
1062
|
+
let scopeDescription = scope ? ` in scope "${scope}"` : '';
|
|
1063
|
+
console.warn(`[remix] Ignoring conflicting import map entry for "${specifier}"${scopeDescription}: ` +
|
|
1064
|
+
`${formatImportMapAddress(authoredAddress)} is already authored, but the discovered map points to ${formatImportMapAddress(address)}`);
|
|
1065
|
+
}
|
|
1066
|
+
return Object.keys(delta).length > 0 ? delta : undefined;
|
|
1067
|
+
}
|
|
1068
|
+
function getImportMapIntegrityDelta(authoredIntegrity, discoveredIntegrity) {
|
|
1069
|
+
let delta = {};
|
|
1070
|
+
for (let [url, integrity] of Object.entries(discoveredIntegrity)) {
|
|
1071
|
+
if (!authoredIntegrity.has(url)) {
|
|
1072
|
+
delta[url] = integrity;
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
let authoredIntegrityValue = authoredIntegrity.get(url);
|
|
1076
|
+
if (authoredIntegrityValue === integrity)
|
|
1077
|
+
continue;
|
|
1078
|
+
console.warn(`[remix] Ignoring conflicting import map integrity entry for "${url}": ` +
|
|
1079
|
+
`"${authoredIntegrityValue}" is already authored, but the discovered map points to "${integrity}"`);
|
|
1080
|
+
}
|
|
1081
|
+
return Object.keys(delta).length > 0 ? delta : undefined;
|
|
1082
|
+
}
|
|
1083
|
+
function parseAuthoredImportMap(json) {
|
|
1084
|
+
let value;
|
|
1085
|
+
try {
|
|
1086
|
+
value = JSON.parse(json);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
if (!isObjectRecord(value))
|
|
1092
|
+
return null;
|
|
1093
|
+
let importMap = {};
|
|
1094
|
+
if (value.imports !== undefined) {
|
|
1095
|
+
if (!isObjectRecord(value.imports))
|
|
1096
|
+
return null;
|
|
1097
|
+
importMap.imports = parseAuthoredImportMapImports(value.imports);
|
|
1098
|
+
}
|
|
1099
|
+
if (value.scopes !== undefined) {
|
|
1100
|
+
if (!isObjectRecord(value.scopes))
|
|
1101
|
+
return null;
|
|
1102
|
+
let scopes = [];
|
|
1103
|
+
for (let [scope, imports] of Object.entries(value.scopes)) {
|
|
1104
|
+
if (!isObjectRecord(imports))
|
|
1105
|
+
continue;
|
|
1106
|
+
scopes.push([scope, parseAuthoredImportMapImports(imports)]);
|
|
1107
|
+
}
|
|
1108
|
+
importMap.scopes = Object.fromEntries(scopes);
|
|
1109
|
+
}
|
|
1110
|
+
if (value.integrity !== undefined) {
|
|
1111
|
+
if (!isObjectRecord(value.integrity))
|
|
1112
|
+
return null;
|
|
1113
|
+
importMap.integrity = Object.fromEntries(Object.entries(value.integrity).filter((entry) => typeof entry[1] === 'string'));
|
|
1114
|
+
}
|
|
1115
|
+
return importMap;
|
|
1116
|
+
}
|
|
1117
|
+
function parseAuthoredImportMapImports(value) {
|
|
1118
|
+
return Object.fromEntries(Object.entries(value).map(([specifier, address]) => [
|
|
1119
|
+
specifier,
|
|
1120
|
+
address === null || typeof address === 'string' ? address : null,
|
|
1121
|
+
]));
|
|
1122
|
+
}
|
|
1123
|
+
function parseFrameworkImportMap(json) {
|
|
1124
|
+
let value;
|
|
1125
|
+
try {
|
|
1126
|
+
value = JSON.parse(json);
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
throw new Error('Invalid framework-owned import map in frame head');
|
|
1130
|
+
}
|
|
1131
|
+
if (!isImportMap(value)) {
|
|
1132
|
+
throw new Error('Invalid framework-owned import map in frame head');
|
|
1133
|
+
}
|
|
1134
|
+
return value;
|
|
1135
|
+
}
|
|
1136
|
+
function isImportMap(value) {
|
|
1137
|
+
if (!isObjectRecord(value))
|
|
1138
|
+
return false;
|
|
1139
|
+
if (value.imports !== undefined && !isImportMapImports(value.imports))
|
|
1140
|
+
return false;
|
|
1141
|
+
if (value.scopes !== undefined) {
|
|
1142
|
+
if (!isObjectRecord(value.scopes))
|
|
1143
|
+
return false;
|
|
1144
|
+
for (let imports of Object.values(value.scopes)) {
|
|
1145
|
+
if (!isImportMapImports(imports))
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (value.integrity !== undefined && !isImportMapIntegrity(value.integrity))
|
|
1150
|
+
return false;
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
function isImportMapImports(value) {
|
|
1154
|
+
if (!isObjectRecord(value))
|
|
1155
|
+
return false;
|
|
1156
|
+
return Object.values(value).every((address) => address === null || typeof address === 'string');
|
|
1157
|
+
}
|
|
1158
|
+
function isImportMapIntegrity(value) {
|
|
1159
|
+
if (!isObjectRecord(value))
|
|
1160
|
+
return false;
|
|
1161
|
+
return Object.values(value).every((integrity) => typeof integrity === 'string');
|
|
1162
|
+
}
|
|
1163
|
+
function isObjectRecord(value) {
|
|
1164
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1165
|
+
}
|
|
1166
|
+
function mergeImportMap(resources, source) {
|
|
1167
|
+
let target = (resources.importMap ??= {});
|
|
1168
|
+
if (source.imports) {
|
|
1169
|
+
target.imports ??= {};
|
|
1170
|
+
mergeImportMapImports(target.imports, source.imports);
|
|
1171
|
+
}
|
|
1172
|
+
if (source.scopes) {
|
|
1173
|
+
target.scopes ??= {};
|
|
1174
|
+
for (let [scope, imports] of Object.entries(source.scopes)) {
|
|
1175
|
+
let targetImports = (target.scopes[scope] ??= {});
|
|
1176
|
+
mergeImportMapImports(targetImports, imports, scope);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (source.integrity) {
|
|
1180
|
+
target.integrity ??= {};
|
|
1181
|
+
mergeImportMapIntegrity(target.integrity, source.integrity);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function mergeImportMapImports(target, source, scope) {
|
|
1185
|
+
for (let [specifier, address] of Object.entries(source)) {
|
|
1186
|
+
if (!Object.hasOwn(target, specifier)) {
|
|
1187
|
+
target[specifier] = address;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (target[specifier] !== address) {
|
|
1191
|
+
let scopeDescription = scope ? ` in scope "${scope}"` : '';
|
|
1192
|
+
throw new Error(`Conflicting framework import map entry for "${specifier}"${scopeDescription}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function mergeImportMapIntegrity(target, source) {
|
|
1197
|
+
for (let [url, integrity] of Object.entries(source)) {
|
|
1198
|
+
if (!Object.hasOwn(target, url)) {
|
|
1199
|
+
target[url] = integrity;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (target[url] !== integrity) {
|
|
1203
|
+
throw new Error(`Conflicting framework import map integrity entry for "${url}"`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1015
1207
|
function escapeScriptJson(json) {
|
|
1016
1208
|
// Avoid prematurely closing the script tag when serialized data contains "</script>".
|
|
1017
1209
|
return json.replace(/</g, '\\u003c');
|
|
1018
1210
|
}
|
|
1211
|
+
function formatImportMapAddress(address) {
|
|
1212
|
+
return address === null ? 'null' : `"${address}"`;
|
|
1213
|
+
}
|
|
1019
1214
|
// Frame styles work end-to-end when frame handlers use their own `renderToStream`:
|
|
1020
1215
|
// the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
|
|
1021
1216
|
// the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the
|