@solidjs/web 2.0.0-beta.29 → 2.0.0-beta.30

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.
@@ -135,6 +135,90 @@ function createJSONSerializer({
135
135
  };
136
136
  }
137
137
 
138
+ const HEAD_ELIGIBLE_TAGS = new Set(["title", "meta", "link", "style", "script", "base"]);
139
+ const HEAD_ATTR_NAME = /^[a-zA-Z_][a-zA-Z0-9_:.-]*$/;
140
+ const RESOURCE_LINK_RELS = new Set(["preload", "modulepreload", "prefetch", "preconnect", "dns-prefetch", "icon", "stylesheet"]);
141
+ const RESOURCE_QUALIFIERS = ["as", "crossorigin", "type", "media", "imagesrcset", "imagesizes"];
142
+ const STYLESHEET_FETCH_META = new Set(["crossorigin", "integrity", "referrerpolicy", "fetchpriority"]);
143
+ function evalHeadValue(v) {
144
+ return typeof v === "function" ? v() : v;
145
+ }
146
+ function evalHeadProps(props, presets) {
147
+ const out = {};
148
+ for (const name in props) out[name] = presets && name in presets ? presets[name] : evalHeadValue(props[name]);
149
+ return out;
150
+ }
151
+ function classifyHeadTag(desc) {
152
+ const tag = desc.tag;
153
+ if (tag === "link") {
154
+ const rel = evalHeadValue(desc.props && desc.props.rel);
155
+ return {
156
+ resource: RESOURCE_LINK_RELS.has(rel),
157
+ rel
158
+ };
159
+ }
160
+ if (tag === "style") return {
161
+ resource: !!(desc.props && "href" in desc.props)
162
+ };
163
+ if (tag === "script") return {
164
+ resource: !!(desc.props && "src" in desc.props)
165
+ };
166
+ return {
167
+ resource: false
168
+ };
169
+ }
170
+ function resourceIdentity(tag, props) {
171
+ let id = "res:" + tag + ":" + (props.rel || "") + ":" + (props.href || props.src || "");
172
+ for (let i = 0; i < RESOURCE_QUALIFIERS.length; i++) {
173
+ const q = RESOURCE_QUALIFIERS[i];
174
+ if (props[q] != null) id += ":" + q + "=" + props[q];
175
+ }
176
+ return id;
177
+ }
178
+ function replaceableIdentity(tag, props, key, unique) {
179
+ if (tag === "title") return "title";
180
+ if (tag === "base") return "base";
181
+ if (tag === "meta" && props.charset != null) return "charset";
182
+ if (key != null) return tag + ":key:" + key;
183
+ if (tag === "meta") {
184
+ if (props.name != null) return "meta:name:" + props.name;
185
+ if (props.property != null) return "meta:property:" + props.property;
186
+ if (props["http-equiv"] != null) return "meta:http-equiv:" + props["http-equiv"];
187
+ return unique;
188
+ }
189
+ if (tag === "link") return "link:" + (props.rel || "") + ":" + (props.href || "");
190
+ return unique;
191
+ }
192
+ function resolveHead(groups) {
193
+ const winners = new Map();
194
+ const sorted = groups.slice().sort((a, b) => a.seq - b.seq);
195
+ for (let i = 0; i < sorted.length; i++) {
196
+ const group = sorted[i];
197
+ const byIdentity = new Map();
198
+ for (let j = 0; j < group.tags.length; j++) {
199
+ const t = group.tags[j];
200
+ let list = byIdentity.get(t.identity);
201
+ if (!list) byIdentity.set(t.identity, list = []);
202
+ list.push(t);
203
+ }
204
+ for (const [identity, tags] of byIdentity) {
205
+ if (identity === "title") {
206
+ if (tags.length > 1) console.warn("Multiple <title> tags in one head group; the last one wins.");
207
+ winners.set(identity, {
208
+ seq: group.seq,
209
+ tags: [tags[tags.length - 1]]
210
+ });
211
+ } else {
212
+ winners.set(identity, {
213
+ seq: group.seq,
214
+ tags
215
+ });
216
+ }
217
+ }
218
+ }
219
+ return winners;
220
+ }
221
+
138
222
  function joinAssetPath(base, file) {
139
223
  if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(file)) return file;
140
224
  if (typeof base !== "string" || !base) base = "/";
@@ -276,7 +360,258 @@ function applyAssetTracking(context, tracking, manifest, noScripts) {
276
360
  context.resolveAssetsSync = resolve;
277
361
  }
278
362
  }
279
- const REPLACE_SCRIPT = `function $df(e,n,o,t){if(!(n=document.getElementById(e)))return 0;if(!(o=document.getElementById("pl-"+e)))return(_$HY.dq=_$HY.dq||{})[e]=1,0;for(;o&&(8!==o.nodeType||o.nodeValue!=="pl-"+e);)t=o.nextSibling,o.remove(),o=t;t=o.parentNode,_$HY.done?o.remove():o.replaceWith(n.content),n.remove(),_$HY.fe(e,t),$dfd();return 1}function $dfl(e,o,n){if(!(o=document.getElementById("pl-"+e)))return(_$HY.dlq=_$HY.dlq||{})[e]=1,0;if(o._$fl)return 1;for(n=o.nextSibling;n;){if(8===n.nodeType&&n.nodeValue==="pl-"+e){o.parentNode&&o.parentNode.insertBefore(o.content.cloneNode(!0),n),o._$fl=1,$dfd();return 1}n=n.nextSibling}return 0}function $dflj(e,i){for(i=0;i<e.length;i++)$dfl(e[i])}function $dfd(e,i){if(e=_$HY.dq){_$HY.dq=0;for(i in e)$df(i)}if(e=_$HY.dlq){_$HY.dlq=0;for(i in e)$dfl(i)}}function $dfs(e,c,d){(_$HY.sc=_$HY.sc||{})[e]=c,d&&((_$HY.sd=_$HY.sd||{})[e]=1)}function $dfg(e,g,i,k){if(!(g=_$HY.sg&&_$HY.sg[e]))return;for(i=0;i<g.length;i++)if(_$HY.sc&&_$HY.sc[g[i]]>0)return;for(i=0;i<g.length;i++)k=g[i],delete _$HY.sg[k],$df(k)}function $dfc(e){if(--_$HY.sc[e]<=0){delete _$HY.sc[e],_$HY.sg&&_$HY.sg[e]?$dfg(e):!(_$HY.sd&&_$HY.sd[e])&&$df(e);_$HY.sd&&delete _$HY.sd[e]}}function $dfj(e,i,n){for(i=0;i<e.length;i++)if(_$HY.sc&&_$HY.sc[e[i]]>0){for(n=0;n<e.length;n++)(_$HY.sg=_$HY.sg||{})[e[n]]=e;return}for(i=0;i<e.length;i++)$df(e[i])}`;
363
+ function isCssUrl(url) {
364
+ const q = url.search(/[?#]/);
365
+ return (q === -1 ? url : url.slice(0, q)).endsWith(".css");
366
+ }
367
+ function createHeadRegistry() {
368
+ return {
369
+ pending: [],
370
+ committed: [],
371
+ seq: 0,
372
+ uniq: 0,
373
+ resources: new Set(),
374
+ eagerHtml: "",
375
+ flushed: null,
376
+ shellFlushed: false
377
+ };
378
+ }
379
+ function registerHeadTags(registry, context, tracking, emitResource, nonce, tags) {
380
+ const boundary = context._currentBoundaryId || "";
381
+ let replaceable = null;
382
+ for (let i = 0; i < tags.length; i++) {
383
+ const desc = tags[i];
384
+ if (!desc || !HEAD_ELIGIBLE_TAGS.has(desc.tag)) {
385
+ console.warn(`useHead: ignoring non-head tag`, desc);
386
+ continue;
387
+ }
388
+ const cls = classifyHeadTag(desc);
389
+ if (cls.resource) {
390
+ emitHeadResource(registry, context, tracking, emitResource, nonce, desc, cls.rel);
391
+ } else {
392
+ (replaceable || (replaceable = [])).push(cls.rel !== undefined ? {
393
+ tag: desc.tag,
394
+ props: desc.props,
395
+ key: desc.key,
396
+ rel: cls.rel
397
+ } : desc);
398
+ }
399
+ }
400
+ if (replaceable) registry.pending.push({
401
+ boundary,
402
+ tags: replaceable
403
+ });
404
+ }
405
+ function emitHeadResource(registry, context, tracking, emitResource, nonce, desc, rel) {
406
+ let props;
407
+ try {
408
+ props = evalHeadProps(desc.props || {}, rel !== undefined ? {
409
+ rel
410
+ } : undefined);
411
+ } catch (err) {
412
+ console.warn(`useHead: error evaluating resource tag props`, err);
413
+ return;
414
+ }
415
+ const identity = resourceIdentity(desc.tag, props);
416
+ if (registry.resources.has(identity)) return;
417
+ registry.resources.add(identity);
418
+ if (desc.tag === "link" && (rel === "stylesheet" || rel === "modulepreload")) {
419
+ let plain = true;
420
+ let gateable = rel === "stylesheet";
421
+ for (const name in props) {
422
+ if (name === "rel" || name === "href") continue;
423
+ plain = false;
424
+ if (!STYLESHEET_FETCH_META.has(name)) gateable = false;
425
+ }
426
+ if (plain && props.href != null) {
427
+ const isCss = isCssUrl(props.href);
428
+ if (rel === "stylesheet" ? isCss : !isCss) {
429
+ context.registerAsset(rel === "stylesheet" ? "style" : "module", props.href);
430
+ return;
431
+ }
432
+ }
433
+ if (gateable && props.href != null) {
434
+ const attrHtml = renderHeadAttrHtml(props);
435
+ const entry = {
436
+ href: props.href,
437
+ attrHtml,
438
+ attrs: headAttrRecord(props)
439
+ };
440
+ if (tracking.currentBoundaryId) {
441
+ let styles = tracking.boundaryStyles.get(tracking.currentBoundaryId);
442
+ if (!styles) tracking.boundaryStyles.set(tracking.currentBoundaryId, styles = new Set());
443
+ styles.add(entry);
444
+ }
445
+ const markup = `<link${attrHtml}>`;
446
+ if (emitResource) emitResource(markup, entry);else {
447
+ registry.eagerHtml += markup;
448
+ entry.emitted = true;
449
+ }
450
+ return;
451
+ }
452
+ }
453
+ const url = props.href || props.src;
454
+ if (url != null && tracking.emittedAssets.has(url)) return;
455
+ const markup = renderHeadTagMarkup(desc.tag, props, null, nonce);
456
+ if (emitResource) emitResource(markup);else registry.eagerHtml += markup;
457
+ }
458
+ function commitHeadBoundary(registry, boundary, isPendingFragment) {
459
+ const keep = [];
460
+ const groups = [];
461
+ for (let i = 0; i < registry.pending.length; i++) {
462
+ const reg = registry.pending[i];
463
+ const mine = boundary === "" ? !(isPendingFragment && reg.boundary !== "" && isPendingFragment(reg.boundary)) : reg.boundary === boundary;
464
+ if (!mine) {
465
+ keep.push(reg);
466
+ continue;
467
+ }
468
+ const tags = [];
469
+ for (let j = 0; j < reg.tags.length; j++) {
470
+ const desc = reg.tags[j];
471
+ let props, key;
472
+ try {
473
+ props = evalHeadProps(desc.props || {}, desc.rel !== undefined ? {
474
+ rel: desc.rel
475
+ } : undefined);
476
+ key = evalHeadValue(desc.key);
477
+ } catch (err) {
478
+ console.warn(`useHead: error evaluating tag props`, err);
479
+ continue;
480
+ }
481
+ const identity = replaceableIdentity(desc.tag, props, key, "u:" + registry.uniq++);
482
+ if ((identity === "base" || identity === "charset") && registry.shellFlushed) {
483
+ console.warn(`useHead: <${desc.tag}> (${identity}) registered after shell flush is ignored`);
484
+ continue;
485
+ }
486
+ tags.push({
487
+ tag: desc.tag,
488
+ props,
489
+ identity
490
+ });
491
+ }
492
+ if (tags.length) groups.push({
493
+ seq: registry.seq++,
494
+ tags
495
+ });
496
+ }
497
+ registry.pending = keep;
498
+ for (let i = 0; i < groups.length; i++) registry.committed.push(groups[i]);
499
+ return groups;
500
+ }
501
+ function adoptHeadBoundary(registry, childKey, parentKey) {
502
+ for (let i = 0; i < registry.pending.length; i++) {
503
+ if (registry.pending[i].boundary === childKey) registry.pending[i].boundary = parentKey;
504
+ }
505
+ }
506
+ function dropHeadBoundary(registry, boundary) {
507
+ registry.pending = registry.pending.filter(reg => reg.boundary !== boundary);
508
+ }
509
+ function headGroupSignature(winner) {
510
+ let sig = "" + winner.seq;
511
+ for (let i = 0; i < winner.tags.length; i++) {
512
+ const t = winner.tags[i];
513
+ sig += "|" + t.tag + JSON.stringify(t.props);
514
+ }
515
+ return sig;
516
+ }
517
+ function renderShellHead(registry, nonce, isPendingFragment) {
518
+ commitHeadBoundary(registry, "", isPendingFragment);
519
+ registry.shellFlushed = true;
520
+ const winners = resolveHead(registry.committed);
521
+ registry.flushed = new Map();
522
+ let prelude = "";
523
+ let links = "";
524
+ let metas = "";
525
+ let others = "";
526
+ let scripts = "";
527
+ for (const [identity, winner] of winners) {
528
+ registry.flushed.set(identity, headGroupSignature(winner));
529
+ for (let i = 0; i < winner.tags.length; i++) {
530
+ const t = winner.tags[i];
531
+ const markup = renderHeadTagMarkup(t.tag, t.props, identity, nonce);
532
+ if (identity === "charset" || identity === "base") prelude += markup;else if (t.tag === "link" || t.tag === "style") links += markup;else if (t.tag === "meta") metas += markup;else if (t.tag === "script") scripts += markup;else others += markup;
533
+ }
534
+ }
535
+ return {
536
+ prelude,
537
+ html: registry.eagerHtml + links + metas + others + scripts
538
+ };
539
+ }
540
+ function flushHeadFragment(registry, boundary) {
541
+ const groups = commitHeadBoundary(registry, boundary);
542
+ if (!groups.length) return null;
543
+ const winners = resolveHead(registry.committed);
544
+ const affected = new Set();
545
+ for (let i = 0; i < groups.length; i++) for (let j = 0; j < groups[i].tags.length; j++) affected.add(groups[i].tags[j].identity);
546
+ const ops = [];
547
+ for (const identity of affected) {
548
+ const winner = winners.get(identity);
549
+ const sig = headGroupSignature(winner);
550
+ if (registry.flushed.get(identity) === sig) continue;
551
+ const existed = registry.flushed.has(identity);
552
+ registry.flushed.set(identity, sig);
553
+ if (identity === "title") {
554
+ const children = winner.tags[0].props.children;
555
+ ops.push(["t", children == null ? "" : String(children)]);
556
+ continue;
557
+ }
558
+ if (existed) ops.push(["r", identity]);
559
+ for (let i = 0; i < winner.tags.length; i++) {
560
+ const t = winner.tags[i];
561
+ const attrs = {};
562
+ for (const name in t.props) {
563
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
564
+ if (!HEAD_ATTR_NAME.test(name)) {
565
+ console.warn(`useHead: ignoring invalid attribute name "${name}"`);
566
+ continue;
567
+ }
568
+ const v = t.props[name];
569
+ if (v == null || v === false) continue;
570
+ attrs[name] = v === true ? "" : String(v);
571
+ }
572
+ const children = t.props.children;
573
+ ops.push(["a", identity, t.tag, attrs, children == null ? null : String(children)]);
574
+ }
575
+ }
576
+ return ops.length ? ops : null;
577
+ }
578
+ function renderHeadAttrHtml(props) {
579
+ let attrs = "";
580
+ for (const name in props) {
581
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
582
+ if (!HEAD_ATTR_NAME.test(name)) {
583
+ console.warn(`useHead: ignoring invalid attribute name "${name}"`);
584
+ continue;
585
+ }
586
+ const v = props[name];
587
+ if (v == null || v === false) continue;
588
+ attrs += v === true ? ` ${name}` : ` ${name}="${escape(String(v), true)}"`;
589
+ }
590
+ return attrs;
591
+ }
592
+ function headAttrRecord(props, skipRelHref) {
593
+ let attrs = null;
594
+ for (const name in props) {
595
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
596
+ if ((name === "rel" || name === "href")) continue;
597
+ if (!HEAD_ATTR_NAME.test(name)) continue;
598
+ const v = props[name];
599
+ if (v == null || v === false) continue;
600
+ (attrs || (attrs = {}))[name] = v === true ? "" : String(v);
601
+ }
602
+ return attrs;
603
+ }
604
+ function renderHeadTagMarkup(tag, props, identity, nonce) {
605
+ let attrs = renderHeadAttrHtml(props);
606
+ if (identity != null) attrs += ` data-dh="${escape(identity, true)}"`;
607
+ if (nonce && (tag === "script" || tag === "style")) attrs += ` nonce="${nonce}"`;
608
+ if (tag === "meta" || tag === "link" || tag === "base") return `<${tag}${attrs}>`;
609
+ let body = props.children == null ? "" : String(props.children);
610
+ if (tag === "script") body = body.replace(/<\/(script)/gi, "<\\/$1");else if (tag === "style") body = escapeStyleContent(body);else body = escape(body);
611
+ return `<${tag}${attrs}>${body}</${tag}>`;
612
+ }
613
+ const REPLACE_SCRIPT = `function $df(e){return _$HY.f?_$HY.f(e):$dfr(e)}function $dfr(e,n,o,t){if(!(n=document.getElementById(e)))return 0;if(!(o=document.getElementById("pl-"+e)))return(_$HY.dq=_$HY.dq||{})[e]=1,0;for(;o&&(8!==o.nodeType||o.nodeValue!=="pl-"+e);)t=o.nextSibling,o.remove(),o=t;t=o.parentNode,o.replaceWith(n.content),n.remove(),_$HY.fe(e,t),_$HY.hp&&_$HY.hp[e]&&($dh(_$HY.hp[e]),delete _$HY.hp[e]),$dfd();return 1}function $dfl(e,o,n){if(!(o=document.getElementById("pl-"+e)))return(_$HY.dlq=_$HY.dlq||{})[e]=1,0;if(o._$fl)return 1;for(n=o.nextSibling;n;){if(8===n.nodeType&&n.nodeValue==="pl-"+e){o.parentNode&&o.parentNode.insertBefore(o.content.cloneNode(!0),n),o._$fl=1,$dfd();return 1}n=n.nextSibling}return 0}function $dflj(e,i){for(i=0;i<e.length;i++)$dfl(e[i])}function $dfd(e,i){if(e=_$HY.dq){_$HY.dq=0;for(i in e)$df(i)}if(e=_$HY.dlq){_$HY.dlq=0;for(i in e)$dfl(i)}}function $dfs(e,c,d){(_$HY.sc=_$HY.sc||{})[e]=c,d&&((_$HY.sd=_$HY.sd||{})[e]=1)}function $dfg(e,g,i,k){if(!(g=_$HY.sg&&_$HY.sg[e]))return;for(i=0;i<g.length;i++)if(_$HY.sc&&_$HY.sc[g[i]]>0)return;for(i=0;i<g.length;i++)k=g[i],delete _$HY.sg[k],$df(k)}function $dfc(e){if(--_$HY.sc[e]<=0){delete _$HY.sc[e],_$HY.sg&&_$HY.sg[e]?$dfg(e):!(_$HY.sd&&_$HY.sd[e])&&$df(e);_$HY.sd&&delete _$HY.sd[e]}}function $dfj(e,i,n){for(i=0;i<e.length;i++)if(_$HY.sc&&_$HY.sc[e[i]]>0){for(n=0;n<e.length;n++)(_$HY.sg=_$HY.sg||{})[e[n]]=e;return}for(i=0;i<e.length;i++)$df(e[i])}`;
614
+ const HEAD_SCRIPT = `function $dha(o,i,e,n){for(i=0;i<o.length;i++)e=o[i],"t"==e[0]?((n=document.querySelector("title"))||(n=document.createElement("title"),document.head.appendChild(n)),n.textContent=e[1],n.setAttribute("data-dh","title")):"r"==e[0]?$dhr(e[1]):(n=document.createElement(e[2]),Object.keys(e[3]).forEach(function(a){n.setAttribute(a,e[3][a])}),null!=e[4]&&(n.textContent=e[4]),n.setAttribute("data-dh",e[1]),document.head.appendChild(n))}function $dhr(v,l,i){for(l=document.head.querySelectorAll("[data-dh]"),i=0;i<l.length;i++)l[i].getAttribute("data-dh")==v&&l[i].remove()}function $dh(o){_$HY.h?_$HY.h(o):$dha(o)}`;
280
615
  function renderToStream(code, options = {}) {
281
616
  let {
282
617
  nonce,
@@ -284,7 +619,8 @@ function renderToStream(code, options = {}) {
284
619
  onCompleteAll,
285
620
  renderId = "",
286
621
  noScripts,
287
- manifest
622
+ manifest,
623
+ onHead
288
624
  } = options;
289
625
  let dispose;
290
626
  const blockingPromises = new Set();
@@ -326,8 +662,8 @@ function renderToStream(code, options = {}) {
326
662
  if (styles.links.length) {
327
663
  emitTask(`$dfs("${key}",${styles.links.length},${deferActivation ? 1 : 0})`);
328
664
  writeTasks();
329
- for (const url of styles.links) {
330
- buffer.write(`<link rel="stylesheet" href="${url}" onload="$dfc('${key}')" onerror="$dfc('${key}')">`);
665
+ for (const entry of styles.links) {
666
+ buffer.write(typeof entry === "string" ? `<link rel="stylesheet" href="${entry}" onload="$dfc('${key}')" onerror="$dfc('${key}')">` : `<link${entry.attrHtml} onload="$dfc('${key}')" onerror="$dfc('${key}')">`);
331
667
  }
332
668
  buffer.write(`<template id="${key}">${value}</template>`);
333
669
  } else {
@@ -345,10 +681,12 @@ function renderToStream(code, options = {}) {
345
681
  buffer.write(`<link rel="modulepreload" href="${value}">`);
346
682
  } else if (type === "inline-style") {
347
683
  buffer.write(renderInlineStyle(value, nonce));
684
+ } else if (type === "head-tag") {
685
+ buffer.write(value);
348
686
  }
349
687
  },
350
688
  shell(shellHtml, meta) {
351
- buffer.write(assembleDocument(shellHtml, meta.assets, meta.preloads, meta.inlineStyles, meta.tasks.length ? meta.tasks : "", nonce));
689
+ buffer.write(assembleDocument(shellHtml, meta.assets, meta.preloads, meta.inlineStyles, meta.tasks.length ? meta.tasks : "", nonce, meta.head, onHead));
352
690
  },
353
691
  ...options.sink
354
692
  };
@@ -419,10 +757,28 @@ function renderToStream(code, options = {}) {
419
757
  }
420
758
  };
421
759
  const tracking = createAssetTracking();
760
+ const headRegistry = createHeadRegistry();
761
+ let headScriptFlushed = false;
762
+ const emitHeadOps = (key, ops) => {
763
+ const payload = JSON.stringify(ops).replace(/</g, "\\u003C");
764
+ emitTask(`${!headScriptFlushed ? HEAD_SCRIPT : ""}(_$HY.hp=_$HY.hp||{})[${JSON.stringify(key)}]=${payload}`);
765
+ headScriptFlushed = true;
766
+ };
422
767
  sharedConfig.context = context = {
423
768
  async: true,
424
769
  assets: [],
425
770
  nonce,
771
+ registerHeadTags(tags) {
772
+ registerHeadTags(headRegistry, context, tracking,
773
+ (markup, gateEntry) => {
774
+ if (!firstFlushed) {
775
+ headRegistry.eagerHtml += markup;
776
+ if (gateEntry) gateEntry.emitted = true;
777
+ } else if (!gateEntry || !tracking.currentBoundaryId) {
778
+ sink.asset("head-tag", markup);
779
+ }
780
+ }, nonce, tags);
781
+ },
426
782
  registerAsset(type, value) {
427
783
  if (type === "inline-style") {
428
784
  const entry = tracking.registerInlineStyle(value);
@@ -513,10 +869,12 @@ function renderToStream(code, options = {}) {
513
869
  parent.children[key] = value !== undefined ? value : "";
514
870
  serializeFragmentAssets(key, tracking.boundaryModules, context);
515
871
  propagateBoundaryStyles(key, parentKey, tracking);
872
+ adoptHeadBoundary(headRegistry, key, parentKey);
516
873
  item.resolve();
517
874
  return;
518
875
  }
519
876
  if (!completed) {
877
+ if (error) dropHeadBoundary(headRegistry, key);
520
878
  if (!firstFlushed) {
521
879
  queue(() => html = replacePlaceholder(html, key, value !== undefined ? value : ""));
522
880
  serializeFragmentAssets(key, tracking.boundaryModules, context);
@@ -524,6 +882,8 @@ function renderToStream(code, options = {}) {
524
882
  } else {
525
883
  serializeFragmentAssets(key, tracking.boundaryModules, context);
526
884
  const styles = collectStreamStyles(key, tracking, headStyles);
885
+ const headOps = error ? null : flushHeadFragment(headRegistry, key);
886
+ if (headOps) emitHeadOps(key, headOps);
527
887
  sink.fragment(key, value !== undefined ? value : " ", {
528
888
  styles,
529
889
  revealGroup,
@@ -611,14 +971,16 @@ function renderToStream(code, options = {}) {
611
971
  const assetsHtml = resolveAssetsHtml(context.assets);
612
972
  headStyles = new Set();
613
973
  for (const url of tracking.emittedAssets) {
614
- if (url.endsWith(".css")) headStyles.add(url);
974
+ if (isCssUrl(url)) headStyles.add(url);
615
975
  }
616
976
  serializeRootAssets();
977
+ const head = renderShellHead(headRegistry, nonce, k => registry.has(k));
617
978
  sink.shell(html, {
618
979
  assets: assetsHtml,
619
980
  preloads: tracking.emittedAssets,
620
981
  inlineStyles: tracking.inlineStyles,
621
- tasks
982
+ tasks,
983
+ head
622
984
  });
623
985
  tasks = "";
624
986
  onCompleteShell && onCompleteShell({
@@ -1004,23 +1366,42 @@ function resolveAssetsHtml(assets) {
1004
1366
  for (let i = 0, len = assets.length; i < len; i++) out += assets[i]();
1005
1367
  return out;
1006
1368
  }
1007
- function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts, nonce) {
1369
+ function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts, nonce, headTags, onHead) {
1008
1370
  const scriptTag = scripts ? `<script${nonce ? ` nonce="${nonce}"` : ""}>${scripts}</script>` : "";
1009
- if (!assetsHtml && !(emittedAssets && emittedAssets.size) && !(inlineStyles && inlineStyles.size)) {
1371
+ const headTagsHtml = headTags ? headTags.html : "";
1372
+ const headPrelude = headTags ? headTags.prelude : "";
1373
+ if (!onHead && !assetsHtml && !headTagsHtml && !headPrelude && !(emittedAssets && emittedAssets.size) && !(inlineStyles && inlineStyles.size)) {
1010
1374
  if (!scriptTag) return html;
1011
1375
  const xs = html.indexOf("<!--xs-->");
1012
1376
  return xs === -1 ? html + scriptTag : html.slice(0, xs) + scriptTag + html.slice(xs);
1013
1377
  }
1378
+ if (headPrelude) {
1379
+ const open = html.match(/<head(?:\s[^>]*)?>/);
1380
+ if (open) {
1381
+ const at = open.index + open[0].length;
1382
+ html = html.slice(0, at) + headPrelude + html.slice(at);
1383
+ }
1384
+ }
1014
1385
  const headIdx = html.indexOf("</head>");
1015
1386
  if (headIdx === -1) {
1387
+ if (onHead) {
1388
+ onHead(headPrelude + headTagsHtml + (assetsHtml || "") + renderHeadAssets(emittedAssets, inlineStyles, nonce));
1389
+ }
1016
1390
  if (!scriptTag) return html;
1017
1391
  const xs = html.indexOf("<!--xs-->");
1018
1392
  return xs === -1 ? html + scriptTag : html.slice(0, xs) + scriptTag + html.slice(xs);
1019
1393
  }
1020
- let head = assetsHtml || "";
1394
+ const head = headTagsHtml + (assetsHtml || "") + renderHeadAssets(emittedAssets, inlineStyles, nonce);
1395
+ if (!scriptTag) return html.slice(0, headIdx) + head + html.slice(headIdx);
1396
+ const xsIdx = html.indexOf("<!--xs-->");
1397
+ if (xsIdx === -1) return html.slice(0, headIdx) + head + html.slice(headIdx) + scriptTag;
1398
+ return xsIdx < headIdx ? html.slice(0, xsIdx) + scriptTag + html.slice(xsIdx, headIdx) + head + html.slice(headIdx) : html.slice(0, headIdx) + head + html.slice(headIdx, xsIdx) + scriptTag + html.slice(xsIdx);
1399
+ }
1400
+ function renderHeadAssets(emittedAssets, inlineStyles, nonce) {
1401
+ let head = "";
1021
1402
  if (emittedAssets && emittedAssets.size) {
1022
1403
  for (const url of emittedAssets) {
1023
- head += url.endsWith(".css") ? `<link rel="stylesheet" href="${url}">` : `<link rel="modulepreload" href="${url}">`;
1404
+ head += isCssUrl(url) ? `<link rel="stylesheet" href="${url}">` : `<link rel="modulepreload" href="${url}">`;
1024
1405
  }
1025
1406
  }
1026
1407
  if (inlineStyles && inlineStyles.size) {
@@ -1030,10 +1411,7 @@ function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts
1030
1411
  head += renderInlineStyle(entry, nonce);
1031
1412
  }
1032
1413
  }
1033
- if (!scriptTag) return html.slice(0, headIdx) + head + html.slice(headIdx);
1034
- const xsIdx = html.indexOf("<!--xs-->");
1035
- if (xsIdx === -1) return html.slice(0, headIdx) + head + html.slice(headIdx) + scriptTag;
1036
- return xsIdx < headIdx ? html.slice(0, xsIdx) + scriptTag + html.slice(xsIdx, headIdx) + head + html.slice(headIdx) : html.slice(0, headIdx) + head + html.slice(headIdx, xsIdx) + scriptTag + html.slice(xsIdx);
1414
+ return head;
1037
1415
  }
1038
1416
  function serializeFragmentAssets(key, boundaryModules, context) {
1039
1417
  const map = boundaryModules.get(key);
@@ -1063,7 +1441,12 @@ function collectStreamStyles(key, tracking, headStyles) {
1063
1441
  for (const entry of styles) {
1064
1442
  if (typeof entry === "string") {
1065
1443
  if (!headStyles || !headStyles.has(entry)) links.push(entry);
1066
- } else if (!entry.emitted) {
1444
+ } else if (entry.emitted) {
1445
+ continue;
1446
+ } else if (entry.attrHtml !== undefined) {
1447
+ entry.emitted = true;
1448
+ links.push(entry);
1449
+ } else {
1067
1450
  entry.emitted = true;
1068
1451
  inline.push(entry);
1069
1452
  }
@@ -1460,7 +1843,12 @@ function createFrameSink(emit, frame) {
1460
1843
  version,
1461
1844
  key
1462
1845
  };
1463
- if (links.length) chunk.styles = links;
1846
+ if (links.length) {
1847
+ chunk.styles = links.map(e => typeof e === "string" ? e : e.attrs ? {
1848
+ href: e.href,
1849
+ attrs: e.attrs
1850
+ } : e.href);
1851
+ }
1464
1852
  if (inline.length) {
1465
1853
  chunk.inlineStyles = inline.map(e => ({
1466
1854
  id: e.id,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solidjs/web",
3
3
  "description": "Solid's web runtime: client rendering, hydration, SSR, and DOM-specific control flow (Portal, Dynamic).",
4
- "version": "2.0.0-beta.29",
4
+ "version": "2.0.0-beta.30",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "homepage": "https://solidjs.com",
@@ -316,10 +316,10 @@
316
316
  "seroval-plugins": "~1.5.4"
317
317
  },
318
318
  "peerDependencies": {
319
- "solid-js": "^2.0.0-beta.29"
319
+ "solid-js": "^2.0.0-beta.30"
320
320
  },
321
321
  "devDependencies": {
322
- "solid-js": "2.0.0-beta.29"
322
+ "solid-js": "2.0.0-beta.30"
323
323
  },
324
324
  "scripts": {
325
325
  "build": "npm-run-all -nl build:clean types:copy-jsx build:js",
package/types/client.d.ts CHANGED
@@ -127,8 +127,28 @@ export function getHydrationKey(): string | undefined;
127
127
  export function getNextElement(template?: () => Element): Element;
128
128
  export function getNextMatch(start: Node, elementName: string): Element;
129
129
  export function getNextMarker(start: Node): [Node, Array<Node>];
130
+ /** @deprecated Use `useHead` — removed before `0.50.0` stable. */
130
131
  export function useAssets(fn: () => JSX.Element): void;
132
+ /** @deprecated Use `useHead` — removed before `0.50.0` stable. */
131
133
  export function getAssets(): string;
134
+ /**
135
+ * A head tag descriptor. Props values may be getters (reactive on the
136
+ * client); `children` is the text body. `key` overrides the built-in dedupe
137
+ * identity (`title` is a hard singleton that `key` cannot fork).
138
+ */
139
+ export type HeadTag = {
140
+ tag: "title" | "meta" | "link" | "style" | "script" | "base";
141
+ props: Record<string, any>;
142
+ key?: string | (() => string);
143
+ };
144
+ /**
145
+ * Registers head tags with the ambient head registry under the current
146
+ * owner. An array is a group — one replacement set. Resolution is
147
+ * last-committed group per identity; disposal restores the previous winner.
148
+ * During hydration the server-flushed head state stays authoritative until
149
+ * hydration completes. See docs/head-management-rfc.md.
150
+ */
151
+ export function useHead(tag: HeadTag | HeadTag[]): void;
132
152
  export type AssetDescriptor =
133
153
  | { type: "style"; href: string; attrs?: Record<string, string> }
134
154
  | { type: "inline-style"; id: string; content?: string; attrs?: Record<string, string> }
@@ -84,6 +84,15 @@ export interface SlotContext {
84
84
  * server content, or the owning frame is disposed.
85
85
  */
86
86
  onCleanup(fn: () => void): void;
87
+ /**
88
+ * Live-props opt-in: a binding that registers here receives the
89
+ * re-resolved props when a re-sent record's args CHANGE in value, instead
90
+ * of the occurrence being re-called — the invocation's instance (and its
91
+ * client state) survives the change. Register synchronously during the
92
+ * invocation; one updater per occurrence (last registration wins). A
93
+ * genuine re-call or unmount clears it before/with the binding it served.
94
+ */
95
+ onUpdate(fn: (props: Record<string, unknown>) => void): void;
87
96
  /**
88
97
  * The range's current interior — server-rendered client content on an
89
98
  * adopted document-SSR boot, or the previous output on a re-call. A
@@ -122,6 +131,22 @@ export interface Frame {
122
131
  readonly error: unknown;
123
132
  /** Whether the named fragment has been revealed into the boundary. */
124
133
  isRevealed(segment: string): boolean;
134
+ /**
135
+ * Re-key this live frame to a different boundary id (the mount-preserving
136
+ * half of a call-site handoff): nothing tears down — the element, store,
137
+ * and slot state stay — while leaving the old id stashes a retention
138
+ * snapshot under it and joining the new id seeds/drains its retained
139
+ * store and buffered chunks. Version affinity resets: histories are per
140
+ * boundary id.
141
+ */
142
+ rebind(id: string): void;
143
+ /**
144
+ * Forget the version baseline without touching content — the next write
145
+ * is accepted whatever its number. Called by the host after seeding a
146
+ * registration from a retained snapshot, whose numbering belongs to a
147
+ * different stream space.
148
+ */
149
+ rebase(): void;
125
150
  /** Tear down: slot cleanups cascade, later chunks are ignored. Idempotent. */
126
151
  dispose(): void;
127
152
  }
@@ -79,6 +79,32 @@ export const SERVER_COMPONENT_SOURCE: unique symbol;
79
79
  /** The call's wire address (`frameAddress`), for regions to be emitted under. */
80
80
  export const SERVER_COMPONENT_ADDRESS: unique symbol;
81
81
 
82
+ /**
83
+ * The handoff contract on components the transport resolves: `{ fnId,
84
+ * frameId, take(prev) }`. A reader whose source resolved a NEW component
85
+ * while a previous one is mounted offers the old one — `take` rebinds the
86
+ * live mount when both are boundaries of the same function (the element and
87
+ * its slot state stay; the incoming stream morphs it), and the reader keeps
88
+ * its previous value instead of remounting. `Symbol.for`, so frameworks can
89
+ * honor it without importing this module.
90
+ */
91
+ export const COMPONENT_HANDOFF: unique symbol;
92
+
93
+ /** The value under `COMPONENT_HANDOFF` on a transport-resolved component. */
94
+ export interface ComponentHandoff {
95
+ /** The server function id both peers derive the boundary's calls from. */
96
+ fnId: string;
97
+ /** The frame id this component's fresh mounts register under. */
98
+ frameId: string;
99
+ /**
100
+ * Offer `prev` (the reader's current value) to this component. Returns
101
+ * true when the reader should KEEP prev — the mounted frame was rebound
102
+ * to this component's id (or already showed it); false means swap
103
+ * normally (different function, unbranded prev, or nothing mounted).
104
+ */
105
+ take(prev: unknown): boolean;
106
+ }
107
+
82
108
  /**
83
109
  * Seroval plugin for a server component crossing a serialization boundary:
84
110
  * a branded component serializes as a REFERENCE — a per-function document