@remix-run/ui 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -14
- package/dist/animation/demos/drag-release.js +5 -7
- package/dist/animation/demos/drag-release.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +9 -4
- package/dist/runtime/component.js +38 -9
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/mix.d.ts +21 -0
- package/dist/runtime/core/mix.js +128 -0
- package/dist/runtime/core/mix.js.map +1 -0
- package/dist/runtime/diff-dom.js +15 -16
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/document-reload.d.ts +2 -0
- package/dist/runtime/document-reload.js +14 -0
- package/dist/runtime/document-reload.js.map +1 -0
- package/dist/runtime/dom.d.ts +21 -11
- package/dist/runtime/event-types.d.ts +14 -0
- package/dist/runtime/event-types.js +2 -0
- package/dist/runtime/event-types.js.map +1 -0
- package/dist/runtime/frame-resolution.js +8 -0
- package/dist/runtime/frame-resolution.js.map +1 -1
- package/dist/runtime/frame.d.ts +24 -6
- package/dist/runtime/frame.js +187 -60
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/import-map-manager.d.ts +8 -0
- package/dist/runtime/import-map-manager.js +295 -0
- package/dist/runtime/import-map-manager.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.js +4 -4
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +9 -4
- package/dist/runtime/mixins/mixin.js +18 -132
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +1 -1
- package/dist/runtime/module-preloader.d.ts +2 -1
- package/dist/runtime/module-preloader.js +6 -5
- package/dist/runtime/module-preloader.js.map +1 -1
- package/dist/runtime/navigation.js +211 -34
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +18 -3
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +3 -0
- package/dist/runtime/run.js +8 -4
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +37 -11
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/spa-response.d.ts +30 -0
- package/dist/runtime/spa-response.js +47 -0
- package/dist/runtime/spa-response.js.map +1 -0
- package/dist/runtime/to-vnode.js +1 -1
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/typed-event-target.d.ts +0 -4
- package/dist/runtime/typed-event-target.js.map +1 -1
- package/dist/server/stream.d.ts +25 -2
- package/dist/server/stream.js +382 -171
- package/dist/server/stream.js.map +1 -1
- package/dist/style/stylesheet.js +2 -2
- package/dist/style/stylesheet.js.map +1 -1
- package/package.json +1 -1
- package/src/animation/demos/drag-release.ts +5 -7
- package/src/index.ts +2 -2
- package/src/runtime/component.ts +52 -14
- package/src/runtime/core/mix.ts +157 -0
- package/src/runtime/demos/readme.demo.tsx +7 -17
- package/src/runtime/diff-dom.ts +13 -15
- package/src/runtime/document-reload.ts +14 -0
- package/src/runtime/dom.ts +21 -11
- package/src/runtime/event-types.ts +27 -0
- package/src/runtime/frame-resolution.ts +9 -0
- package/src/runtime/frame.ts +216 -65
- package/src/runtime/import-map-manager.ts +369 -0
- package/src/runtime/mixins/link-mixin.ts +4 -4
- package/src/runtime/mixins/mixin.ts +44 -149
- package/src/runtime/mixins/on-mixin.ts +1 -1
- package/src/runtime/module-preloader.ts +9 -6
- package/src/runtime/navigation.ts +257 -42
- package/src/runtime/reconcile.ts +20 -4
- package/src/runtime/run.ts +13 -4
- package/src/runtime/scheduler.ts +53 -13
- package/src/runtime/spa-response.ts +56 -0
- package/src/runtime/to-vnode.ts +1 -1
- package/src/runtime/typed-event-target.ts +1 -6
- package/src/server/README.md +25 -5
- package/src/server/stream.ts +506 -202
- package/src/style/stylesheet.ts +3 -3
- package/src/test/utils.ts +1 -6
- package/dist/runtime/event-listeners.d.ts +0 -50
- package/dist/runtime/event-listeners.js +0 -31
- package/dist/runtime/event-listeners.js.map +0 -1
- package/src/runtime/event-listeners.ts +0 -171
package/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
|
}
|
|
@@ -300,14 +322,17 @@ function buildFrameSegment(node, context, frameState) {
|
|
|
300
322
|
context.pendingFrames.push({ frameId, promise: framePromise });
|
|
301
323
|
}
|
|
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);
|
|
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
|
-
|
|
392
|
-
if (descriptors.length === 0)
|
|
486
|
+
if (resolveMixDescriptors(initialProps).length === 0)
|
|
393
487
|
return initialProps;
|
|
394
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
495
|
+
return runner(...descriptor.args, mixinProps);
|
|
405
496
|
}
|
|
406
497
|
catch (error) {
|
|
407
498
|
console.error(error);
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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(
|
|
916
|
-
return Array.from(
|
|
911
|
+
function collectModulePreloadTags(resources) {
|
|
912
|
+
return Array.from(resources.modulePreloadTags).join('');
|
|
917
913
|
}
|
|
918
|
-
function
|
|
914
|
+
function hoistClientEntryResourcesFromFrameHead(html, resources) {
|
|
919
915
|
if (!html.startsWith(FRAME_HEAD_OPEN_TAG))
|
|
920
916
|
return html;
|
|
921
|
-
let
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
929
|
-
|
|
936
|
+
preloadTags.push(html.slice(cursor, tagEnd));
|
|
937
|
+
cursor = tagEnd;
|
|
930
938
|
}
|
|
931
|
-
if (
|
|
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
|
|
937
|
-
|
|
941
|
+
for (let tag of preloadTags) {
|
|
942
|
+
resources.modulePreloadTags.add(tag);
|
|
938
943
|
}
|
|
939
|
-
|
|
940
|
-
|
|
944
|
+
for (let importMap of importMaps) {
|
|
945
|
+
mergeImportMap(resources, importMap);
|
|
941
946
|
}
|
|
942
|
-
|
|
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
|
-
//
|
|
983
|
-
return css.replace(
|
|
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
|