@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.
Files changed (52) hide show
  1. package/README.md +56 -8
  2. package/dist/runtime/component.d.ts +9 -4
  3. package/dist/runtime/component.js +38 -9
  4. package/dist/runtime/component.js.map +1 -1
  5. package/dist/runtime/core/mix.d.ts +21 -0
  6. package/dist/runtime/core/mix.js +128 -0
  7. package/dist/runtime/core/mix.js.map +1 -0
  8. package/dist/runtime/diff-dom.js +7 -13
  9. package/dist/runtime/diff-dom.js.map +1 -1
  10. package/dist/runtime/document-reload.d.ts +2 -0
  11. package/dist/runtime/document-reload.js +14 -0
  12. package/dist/runtime/document-reload.js.map +1 -0
  13. package/dist/runtime/frame.d.ts +19 -4
  14. package/dist/runtime/frame.js +80 -11
  15. package/dist/runtime/frame.js.map +1 -1
  16. package/dist/runtime/import-map-manager.d.ts +8 -0
  17. package/dist/runtime/import-map-manager.js +295 -0
  18. package/dist/runtime/import-map-manager.js.map +1 -0
  19. package/dist/runtime/mixins/mixin.d.ts +0 -1
  20. package/dist/runtime/mixins/mixin.js +18 -132
  21. package/dist/runtime/mixins/mixin.js.map +1 -1
  22. package/dist/runtime/module-preloader.d.ts +2 -1
  23. package/dist/runtime/module-preloader.js +3 -2
  24. package/dist/runtime/module-preloader.js.map +1 -1
  25. package/dist/runtime/navigation.js +143 -48
  26. package/dist/runtime/navigation.js.map +1 -1
  27. package/dist/runtime/reconcile.js +9 -0
  28. package/dist/runtime/reconcile.js.map +1 -1
  29. package/dist/runtime/run.d.ts +3 -0
  30. package/dist/runtime/run.js +3 -1
  31. package/dist/runtime/run.js.map +1 -1
  32. package/dist/runtime/to-vnode.js +1 -1
  33. package/dist/runtime/to-vnode.js.map +1 -1
  34. package/dist/server/stream.d.ts +25 -2
  35. package/dist/server/stream.js +361 -166
  36. package/dist/server/stream.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/runtime/component.ts +52 -14
  39. package/src/runtime/core/mix.ts +157 -0
  40. package/src/runtime/diff-dom.ts +7 -12
  41. package/src/runtime/document-reload.ts +14 -0
  42. package/src/runtime/frame.ts +105 -16
  43. package/src/runtime/import-map-manager.ts +369 -0
  44. package/src/runtime/mixins/mixin.ts +18 -145
  45. package/src/runtime/module-preloader.ts +6 -3
  46. package/src/runtime/navigation.ts +178 -58
  47. package/src/runtime/reconcile.ts +9 -0
  48. package/src/runtime/run.ts +7 -1
  49. package/src/runtime/to-vnode.ts +1 -1
  50. package/src/server/README.md +25 -5
  51. package/src/server/stream.ts +478 -197
  52. package/src/test/utils.ts +1 -6
@@ -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
- return { html: stripFlushMarkers(stripDoctypeMarkup(input)) };
227
- return await splitFirstChunk(input);
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 = hoistModulePreloadsFromFrameHead(html, context);
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
- let descriptors = resolveSsrMixDescriptors(initialProps);
404
- if (descriptors.length === 0)
486
+ if (resolveMixDescriptors(initialProps).length === 0)
405
487
  return initialProps;
406
- let composedProps = withoutSsrMix(initialProps);
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
- continue;
414
- let result;
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
- result = runner(...descriptor.args, mixinProps);
495
+ return runner(...descriptor.args, mixinProps);
417
496
  }
418
497
  catch (error) {
419
498
  console.error(error);
420
- continue;
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
- let nextProps = sanitizeReturnedSsrMixinProps(remixResult.props);
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
- if (preloads || styles) {
876
- let headContent = preloads + styles;
877
- if (hasHtmlRoot) {
878
- // For HTML root, inject into existing head or create one
879
- let headCloseIndex = html.indexOf('</head>');
880
- if (headCloseIndex !== -1) {
881
- // Inject before existing </head>
882
- html = html.slice(0, headCloseIndex) + headContent + html.slice(headCloseIndex);
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
- // No existing head, inject after <html>
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
- else {
895
- // No HTML root, prepend head
896
- html = `<head>${headContent}</head>${html}`;
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(context) {
932
- return Array.from(context.modulePreloadTags).join('');
911
+ function collectModulePreloadTags(resources) {
912
+ return Array.from(resources.modulePreloadTags).join('');
933
913
  }
934
- function hoistModulePreloadsFromFrameHead(html, context) {
914
+ function hoistClientEntryResourcesFromFrameHead(html, resources) {
935
915
  if (!html.startsWith(FRAME_HEAD_OPEN_TAG))
936
916
  return html;
937
- let tags = [];
938
- let remainingHeadStart = FRAME_HEAD_OPEN_TAG.length;
939
- while (html.startsWith(MARKED_MODULE_PRELOAD_START, remainingHeadStart)) {
940
- let tagEnd = html.indexOf(MODULE_PRELOAD_END, remainingHeadStart + MARKED_MODULE_PRELOAD_START.length);
941
- if (tagEnd === -1)
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
- tags.push(html.slice(remainingHeadStart, tagEnd));
945
- remainingHeadStart = tagEnd;
936
+ preloadTags.push(html.slice(cursor, tagEnd));
937
+ cursor = tagEnd;
946
938
  }
947
- if (tags.length === 0)
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 tags) {
953
- context.modulePreloadTags.add(tag);
941
+ for (let tag of preloadTags) {
942
+ resources.modulePreloadTags.add(tag);
954
943
  }
955
- if (remainingHeadStart === headClose) {
956
- return html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length);
944
+ for (let importMap of importMaps) {
945
+ mergeImportMap(resources, importMap);
957
946
  }
958
- return FRAME_HEAD_OPEN_TAG + html.slice(remainingHeadStart);
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