@remix-run/ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +90 -14
  2. package/dist/animation/demos/drag-release.js +5 -7
  3. package/dist/animation/demos/drag-release.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/runtime/component.d.ts +9 -4
  8. package/dist/runtime/component.js +38 -9
  9. package/dist/runtime/component.js.map +1 -1
  10. package/dist/runtime/core/mix.d.ts +21 -0
  11. package/dist/runtime/core/mix.js +128 -0
  12. package/dist/runtime/core/mix.js.map +1 -0
  13. package/dist/runtime/diff-dom.js +15 -16
  14. package/dist/runtime/diff-dom.js.map +1 -1
  15. package/dist/runtime/document-reload.d.ts +2 -0
  16. package/dist/runtime/document-reload.js +14 -0
  17. package/dist/runtime/document-reload.js.map +1 -0
  18. package/dist/runtime/dom.d.ts +21 -11
  19. package/dist/runtime/event-types.d.ts +14 -0
  20. package/dist/runtime/event-types.js +2 -0
  21. package/dist/runtime/event-types.js.map +1 -0
  22. package/dist/runtime/frame-resolution.js +8 -0
  23. package/dist/runtime/frame-resolution.js.map +1 -1
  24. package/dist/runtime/frame.d.ts +24 -6
  25. package/dist/runtime/frame.js +187 -60
  26. package/dist/runtime/frame.js.map +1 -1
  27. package/dist/runtime/import-map-manager.d.ts +8 -0
  28. package/dist/runtime/import-map-manager.js +295 -0
  29. package/dist/runtime/import-map-manager.js.map +1 -0
  30. package/dist/runtime/mixins/link-mixin.js +4 -4
  31. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  32. package/dist/runtime/mixins/mixin.d.ts +9 -4
  33. package/dist/runtime/mixins/mixin.js +18 -132
  34. package/dist/runtime/mixins/mixin.js.map +1 -1
  35. package/dist/runtime/mixins/on-mixin.d.ts +1 -1
  36. package/dist/runtime/module-preloader.d.ts +2 -1
  37. package/dist/runtime/module-preloader.js +6 -5
  38. package/dist/runtime/module-preloader.js.map +1 -1
  39. package/dist/runtime/navigation.js +211 -34
  40. package/dist/runtime/navigation.js.map +1 -1
  41. package/dist/runtime/reconcile.js +18 -3
  42. package/dist/runtime/reconcile.js.map +1 -1
  43. package/dist/runtime/run.d.ts +3 -0
  44. package/dist/runtime/run.js +8 -4
  45. package/dist/runtime/run.js.map +1 -1
  46. package/dist/runtime/scheduler.js +37 -11
  47. package/dist/runtime/scheduler.js.map +1 -1
  48. package/dist/runtime/spa-response.d.ts +30 -0
  49. package/dist/runtime/spa-response.js +47 -0
  50. package/dist/runtime/spa-response.js.map +1 -0
  51. package/dist/runtime/to-vnode.js +1 -1
  52. package/dist/runtime/to-vnode.js.map +1 -1
  53. package/dist/runtime/typed-event-target.d.ts +0 -4
  54. package/dist/runtime/typed-event-target.js.map +1 -1
  55. package/dist/server/stream.d.ts +25 -2
  56. package/dist/server/stream.js +382 -171
  57. package/dist/server/stream.js.map +1 -1
  58. package/dist/style/stylesheet.js +2 -2
  59. package/dist/style/stylesheet.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/animation/demos/drag-release.ts +5 -7
  62. package/src/index.ts +2 -2
  63. package/src/runtime/component.ts +52 -14
  64. package/src/runtime/core/mix.ts +157 -0
  65. package/src/runtime/demos/readme.demo.tsx +7 -17
  66. package/src/runtime/diff-dom.ts +13 -15
  67. package/src/runtime/document-reload.ts +14 -0
  68. package/src/runtime/dom.ts +21 -11
  69. package/src/runtime/event-types.ts +27 -0
  70. package/src/runtime/frame-resolution.ts +9 -0
  71. package/src/runtime/frame.ts +216 -65
  72. package/src/runtime/import-map-manager.ts +369 -0
  73. package/src/runtime/mixins/link-mixin.ts +4 -4
  74. package/src/runtime/mixins/mixin.ts +44 -149
  75. package/src/runtime/mixins/on-mixin.ts +1 -1
  76. package/src/runtime/module-preloader.ts +9 -6
  77. package/src/runtime/navigation.ts +257 -42
  78. package/src/runtime/reconcile.ts +20 -4
  79. package/src/runtime/run.ts +13 -4
  80. package/src/runtime/scheduler.ts +53 -13
  81. package/src/runtime/spa-response.ts +56 -0
  82. package/src/runtime/to-vnode.ts +1 -1
  83. package/src/runtime/typed-event-target.ts +1 -6
  84. package/src/server/README.md +25 -5
  85. package/src/server/stream.ts +506 -202
  86. package/src/style/stylesheet.ts +3 -3
  87. package/src/test/utils.ts +1 -6
  88. package/dist/runtime/event-listeners.d.ts +0 -50
  89. package/dist/runtime/event-listeners.js +0 -31
  90. package/dist/runtime/event-listeners.js.map +0 -1
  91. package/src/runtime/event-listeners.ts +0 -171
@@ -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
  }
@@ -300,14 +322,17 @@ function buildFrameSegment(node, context, frameState) {
300
322
  context.pendingFrames.push({ frameId, promise: framePromise });
301
323
  }
302
324
  else {
303
- seg.pending = Promise.resolve(context.resolveFrame(props.src, props.name, resolveFrameContext)).then(async (resolved) => {
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);
309
331
  }
310
332
  });
333
+ // An earlier blocking frame may reject before this promise is awaited.
334
+ framePromise.catch(() => { });
335
+ seg.pending = framePromise;
311
336
  }
312
337
  return seg;
313
338
  }
@@ -328,6 +353,18 @@ function buildElementSegment(tag, props, context, frameState) {
328
353
  if (props.innerHTML) {
329
354
  return staticSeg(`<${tag}${attrs}>${props.innerHTML}</${tag}>`);
330
355
  }
356
+ if (tag === 'script') {
357
+ if (typeof props.children === 'string') {
358
+ if (context.insideHead) {
359
+ collectAuthoredImportMap(context, tag, processedProps, props.children);
360
+ }
361
+ return staticSeg(`<${tag}${attrs}>${escapeScriptTextContent(props.children)}</${tag}>`);
362
+ }
363
+ if (props.children != null) {
364
+ console.error(new Error('script elements with children must have a single string child'));
365
+ }
366
+ return staticSeg(`<${tag}${attrs}></${tag}>`);
367
+ }
331
368
  let open = staticSeg(`<${tag}${attrs}>`);
332
369
  // Adjust svg context for children: foreignObject switches back to HTML
333
370
  let previousInsideSvg = context.insideSvg;
@@ -342,6 +379,61 @@ function buildTextareaElementSegment(tag, props) {
342
379
  let value = props.value ?? props.defaultValue ?? '';
343
380
  return staticSeg(`<${tag}${attrs}>${escapeTextContent(String(value))}</${tag}>`);
344
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
+ }
345
437
  function renderInputAttributes(props) {
346
438
  let value = props.value === undefined && props.defaultValue !== undefined ? props.defaultValue : props.value;
347
439
  let checked = props.checked === undefined && props.defaultChecked !== undefined
@@ -358,7 +450,10 @@ function buildHeadElementSegment(tag, props, context, frameState) {
358
450
  let processedProps = processStyleProps(props);
359
451
  let attrs = renderAttributes(processedProps, false);
360
452
  let open = staticSeg(`<${tag}${attrs}>`);
453
+ let previousInsideHead = context.insideHead;
454
+ context.insideHead = true;
361
455
  let children = props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('');
456
+ context.insideHead = previousInsideHead;
362
457
  let close = staticSeg(`</${tag}>`);
363
458
  return compositeSeg([open, children, close]);
364
459
  }
@@ -388,64 +483,22 @@ function renderAttributes(props, isSvg, excludedProps) {
388
483
  return attrs;
389
484
  }
390
485
  function resolveSsrMixedProps(hostType, initialProps, context, frameState) {
391
- let descriptors = resolveSsrMixDescriptors(initialProps);
392
- if (descriptors.length === 0)
486
+ if (resolveMixDescriptors(initialProps).length === 0)
393
487
  return initialProps;
394
- let composedProps = withoutSsrMix(initialProps);
395
- let mixinProps = withoutSsrMixinTreeProps(composedProps);
396
- let maxDescriptors = 1024;
397
- for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {
398
- let descriptor = descriptors[index];
488
+ return composeMixedProps(hostType, initialProps, (descriptor, _index, mixinProps) => {
399
489
  let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState);
400
490
  if (!runner)
401
- continue;
402
- 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.
403
494
  try {
404
- result = runner(...descriptor.args, mixinProps);
495
+ return runner(...descriptor.args, mixinProps);
405
496
  }
406
497
  catch (error) {
407
498
  console.error(error);
408
- continue;
409
- }
410
- if (!result)
411
- continue;
412
- if (isSsrMixinElement(result))
413
- continue;
414
- let returnedDescriptors = resolveReturnedSsrMixDescriptors(result);
415
- if (returnedDescriptors) {
416
- for (let returned of returnedDescriptors)
417
- descriptors.push(returned);
418
- continue;
419
- }
420
- if (!isRemixElement(result)) {
421
- console.error(new Error('mixins must return a remix element'));
422
- continue;
423
- }
424
- let remixResult = result;
425
- let resultType = typeof remixResult.type === 'string'
426
- ? remixResult.type
427
- : isSsrMixinElement(remixResult.type)
428
- ? remixResult.type.__rmxMixinElementType
429
- : null;
430
- if (resultType !== hostType) {
431
- console.error(new Error('mixins must return an element with the same host type'));
432
- continue;
433
- }
434
- if (remixResult.type !== resultType) {
435
- remixResult = { ...remixResult, type: resultType };
499
+ return undefined;
436
500
  }
437
- let nextProps = sanitizeReturnedSsrMixinProps(remixResult.props);
438
- let nestedDescriptors = resolveSsrMixDescriptors(nextProps);
439
- for (let nested of nestedDescriptors)
440
- descriptors.push(nested);
441
- composedProps = { ...composedProps, ...withoutSsrMix(nextProps) };
442
- mixinProps = withoutSsrMixinTreeProps(composedProps);
443
- }
444
- let nextMix = initialProps.mix;
445
- return {
446
- ...composedProps,
447
- ...(nextMix === undefined ? {} : { mix: nextMix }),
448
- };
501
+ });
449
502
  }
450
503
  function resolveSsrMixinRunner(hostType, descriptor, context, frameState) {
451
504
  if (typeof descriptor.type !== 'function')
@@ -508,78 +561,9 @@ function createSsrMixinHandle(hostType, _descriptor, context, frameState) {
508
561
  dispatchEvent: () => true,
509
562
  };
510
563
  }
511
- function resolveSsrMixDescriptors(props) {
512
- let mix = props.mix;
513
- if (!mix)
514
- return [];
515
- if (Array.isArray(mix)) {
516
- if (mix.length === 0)
517
- return [];
518
- return mix.filter(Boolean);
519
- }
520
- return [mix];
521
- }
522
- function withoutSsrMix(props) {
523
- if (!('mix' in props))
524
- return props;
525
- let output = { ...props };
526
- delete output.mix;
527
- return output;
528
- }
529
- function withoutSsrMixinTreeProps(props) {
530
- if (!('children' in props) && !('innerHTML' in props))
531
- return props;
532
- let output = { ...props };
533
- delete output.children;
534
- delete output.innerHTML;
535
- return output;
536
- }
537
- function sanitizeReturnedSsrMixinProps(props) {
538
- if (!('children' in props) && !('innerHTML' in props))
539
- return props;
540
- console.error(new Error('mixins must not return children or innerHTML'));
541
- return withoutSsrMixinTreeProps(props);
542
- }
543
- function resolveReturnedSsrMixDescriptors(value) {
544
- let descriptors = [];
545
- if (!collectReturnedSsrMixDescriptors(value, descriptors)) {
546
- return null;
547
- }
548
- return descriptors;
549
- }
550
- function collectReturnedSsrMixDescriptors(value, output) {
551
- if (!value) {
552
- return true;
553
- }
554
- if (Array.isArray(value)) {
555
- for (let item of value) {
556
- if (!collectReturnedSsrMixDescriptors(item, output)) {
557
- return false;
558
- }
559
- }
560
- return true;
561
- }
562
- if (!isSsrMixinDescriptor(value)) {
563
- return false;
564
- }
565
- output.push(value);
566
- return true;
567
- }
568
- function isSsrMixinElement(value) {
569
- if (typeof value !== 'function')
570
- return false;
571
- return '__rmxMixinElementType' in value;
572
- }
573
564
  function isElementFunction(value) {
574
565
  return typeof value === 'function';
575
566
  }
576
- function isSsrMixinDescriptor(value) {
577
- if (!value || typeof value !== 'object' || isRemixElement(value)) {
578
- return false;
579
- }
580
- let descriptor = value;
581
- return typeof descriptor.type === 'function' && Array.isArray(descriptor.args);
582
- }
583
567
  function buildComponentSegment(type, props, context, componentId, frameState) {
584
568
  let vnode = createVNode(type, props);
585
569
  if (context.parentVNode) {
@@ -766,18 +750,24 @@ async function resolveClientEntries(context, resolveClientEntry) {
766
750
  : resolveDefaultClientEntry(entryId, component);
767
751
  validateResolvedClientEntry(entryId, resolvedEntry);
768
752
  resolvedEntries.set(component, resolvedEntry);
753
+ collectResolvedClientEntryResources(context.clientEntryHeadResources, resolvedEntry);
769
754
  }
770
755
  context.hydrationData.set(hydrationId, {
771
756
  exportName: resolvedEntry.exportName,
772
757
  moduleUrl: resolvedEntry.href,
773
758
  props,
774
759
  });
775
- for (let preload of resolvedEntry.preloads ?? []) {
776
- context.modulePreloadTags.add(createModulePreloadTag(preload));
777
- }
778
760
  }
779
761
  context.unresolvedHydrationData.clear();
780
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
+ }
781
771
  function validateResolvedClientEntry(entryId, resolvedEntry) {
782
772
  if (!resolvedEntry || typeof resolvedEntry !== 'object') {
783
773
  throw new Error(`resolveClientEntry must return an object with href and exportName. Received "${entryId}".`);
@@ -798,6 +788,9 @@ function validateResolvedClientEntry(entryId, resolvedEntry) {
798
788
  }
799
789
  }
800
790
  }
791
+ if (resolvedEntry.importMap !== undefined && !isImportMap(resolvedEntry.importMap)) {
792
+ throw new Error(`resolveClientEntry importMap must be a valid import map. Received "${entryId}".`);
793
+ }
801
794
  }
802
795
  function validateClientEntriesForHydration(context) {
803
796
  if (context.unresolvedHydrationData.size > 0) {
@@ -849,36 +842,37 @@ function escapeTextContent(str) {
849
842
  function escapeTemplateContent(html) {
850
843
  return html.replace(/<\/template/gi, '<\\/template');
851
844
  }
845
+ const SCRIPT_TAG_PATTERN = /(<\/|<)(s)(cript)/gi;
846
+ function escapeScriptTextContent(value) {
847
+ return value.replace(SCRIPT_TAG_PATTERN, (_match, prefix, firstLetter, suffix) => `${prefix}${firstLetter === 's' ? '\\u0073' : '\\u0053'}${suffix}`);
848
+ }
852
849
  function transformAttributeName(name, isSvg) {
853
850
  return normalizeAttributeName(name, isSvg).attr;
854
851
  }
855
852
  function finalizeHtml(html, context) {
856
853
  let hasHtmlRoot = context.flushKind === 'document';
857
- let preloads = collectModulePreloadTags(context);
854
+ let preloads = collectModulePreloadTags(context.clientEntryHeadResources);
858
855
  let styles = collectStyleTags(context);
859
- if (preloads || styles) {
860
- let headContent = preloads + styles;
861
- if (hasHtmlRoot) {
862
- // For HTML root, inject into existing head or create one
863
- let headCloseIndex = html.indexOf('</head>');
864
- if (headCloseIndex !== -1) {
865
- // Inject before existing </head>
866
- 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);
867
868
  }
868
869
  else {
869
- // No existing head, inject after <html>
870
- let htmlOpenMatch = html.match(/<html[^>]*>/);
871
- if (htmlOpenMatch) {
872
- let insertIndex = htmlOpenMatch.index + htmlOpenMatch[0].length;
873
- html =
874
- html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex);
875
- }
870
+ html = headContent + html;
876
871
  }
877
872
  }
878
- else {
879
- // No HTML root, prepend head
880
- html = `<head>${headContent}</head>${html}`;
881
- }
873
+ }
874
+ if (!hasHtmlRoot && headContent) {
875
+ html = `<head>${headContent}</head>${html}`;
882
876
  }
883
877
  // Append aggregated hydration/frame data script at the end
884
878
  let rmxData = buildRmxDataScript(context);
@@ -907,39 +901,54 @@ function finalizeHtml(html, context) {
907
901
  }
908
902
  const FRAME_HEAD_OPEN_TAG = '<head>';
909
903
  const FRAME_HEAD_CLOSE_TAG = '</head>';
910
- const MARKED_MODULE_PRELOAD_START = '<link data-rmx rel="modulepreload" href="';
904
+ const MARKED_MODULE_PRELOAD_START = '<link data-rmx-module-preload rel="modulepreload" href="';
911
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>';
912
908
  function createModulePreloadTag(href) {
913
909
  return `${MARKED_MODULE_PRELOAD_START}${escapeHtml(href)}${MODULE_PRELOAD_END}`;
914
910
  }
915
- function collectModulePreloadTags(context) {
916
- return Array.from(context.modulePreloadTags).join('');
911
+ function collectModulePreloadTags(resources) {
912
+ return Array.from(resources.modulePreloadTags).join('');
917
913
  }
918
- function hoistModulePreloadsFromFrameHead(html, context) {
914
+ function hoistClientEntryResourcesFromFrameHead(html, resources) {
919
915
  if (!html.startsWith(FRAME_HEAD_OPEN_TAG))
920
916
  return html;
921
- let tags = [];
922
- let remainingHeadStart = FRAME_HEAD_OPEN_TAG.length;
923
- while (html.startsWith(MARKED_MODULE_PRELOAD_START, remainingHeadStart)) {
924
- let tagEnd = html.indexOf(MODULE_PRELOAD_END, remainingHeadStart + MARKED_MODULE_PRELOAD_START.length);
925
- 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)
926
934
  return html;
927
935
  tagEnd += MODULE_PRELOAD_END.length;
928
- tags.push(html.slice(remainingHeadStart, tagEnd));
929
- remainingHeadStart = tagEnd;
936
+ preloadTags.push(html.slice(cursor, tagEnd));
937
+ cursor = tagEnd;
930
938
  }
931
- if (tags.length === 0)
932
- return html;
933
- let headClose = html.indexOf(FRAME_HEAD_CLOSE_TAG, remainingHeadStart);
934
- if (headClose === -1)
939
+ if (preloadTags.length === 0 && importMaps.length === 0)
935
940
  return html;
936
- for (let tag of tags) {
937
- context.modulePreloadTags.add(tag);
941
+ for (let tag of preloadTags) {
942
+ resources.modulePreloadTags.add(tag);
938
943
  }
939
- if (remainingHeadStart === headClose) {
940
- return html.slice(headClose + FRAME_HEAD_CLOSE_TAG.length);
944
+ for (let importMap of importMaps) {
945
+ mergeImportMap(resources, importMap);
941
946
  }
942
- 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}`;
943
952
  }
944
953
  function processStyleProps(props) {
945
954
  let processedProps = { ...props };
@@ -976,11 +985,11 @@ function renderStyleTag(selector, css, layer = REMIX_UI_STYLE_LAYER) {
976
985
  let wrappedCss = wrapStyleForLayer(selector, css, layer);
977
986
  if (!wrappedCss)
978
987
  return '';
979
- return `<style data-rmx="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`;
988
+ return `<style data-rmx-style="${escapeHtml(selector)}">${escapeStyleText(wrappedCss)}</style>`;
980
989
  }
981
990
  function escapeStyleText(css) {
982
- // A literal "</style" closes an HTML style element even when it appears inside a CSS string.
983
- return css.replace(/</g, '\\3C ');
991
+ // Only neutralize literal style end tags. Escaping every '<' breaks valid range media queries.
992
+ return css.replace(/<\/style/gi, '\\3C/style');
984
993
  }
985
994
  function buildRmxDataScript(context) {
986
995
  if (context.hydrationData.size === 0 && context.frameData.size === 0) {
@@ -996,10 +1005,212 @@ function buildRmxDataScript(context) {
996
1005
  let serializedData = escapeScriptJson(JSON.stringify(data));
997
1006
  return `<script type="application/json" id="rmx-data">${serializedData}</script>`;
998
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
+ }
999
1207
  function escapeScriptJson(json) {
1000
1208
  // Avoid prematurely closing the script tag when serialized data contains "</script>".
1001
1209
  return json.replace(/</g, '\\u003c');
1002
1210
  }
1211
+ function formatImportMapAddress(address) {
1212
+ return address === null ? 'null' : `"${address}"`;
1213
+ }
1003
1214
  // Frame styles work end-to-end when frame handlers use their own `renderToStream`:
1004
1215
  // the handler's `finalizeHtml` emits selector-addressed `<style>` tags in its HTML, and on the client,
1005
1216
  // the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the