@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.
package/dist/server.cjs CHANGED
@@ -101,6 +101,90 @@ function getLocalHeaderScript(id) {
101
101
  }
102
102
  seroval.Feature.RegExp;
103
103
 
104
+ const HEAD_ELIGIBLE_TAGS = new Set(["title", "meta", "link", "style", "script", "base"]);
105
+ const HEAD_ATTR_NAME = /^[a-zA-Z_][a-zA-Z0-9_:.-]*$/;
106
+ const RESOURCE_LINK_RELS = new Set(["preload", "modulepreload", "prefetch", "preconnect", "dns-prefetch", "icon", "stylesheet"]);
107
+ const RESOURCE_QUALIFIERS = ["as", "crossorigin", "type", "media", "imagesrcset", "imagesizes"];
108
+ const STYLESHEET_FETCH_META = new Set(["crossorigin", "integrity", "referrerpolicy", "fetchpriority"]);
109
+ function evalHeadValue(v) {
110
+ return typeof v === "function" ? v() : v;
111
+ }
112
+ function evalHeadProps(props, presets) {
113
+ const out = {};
114
+ for (const name in props) out[name] = presets && name in presets ? presets[name] : evalHeadValue(props[name]);
115
+ return out;
116
+ }
117
+ function classifyHeadTag(desc) {
118
+ const tag = desc.tag;
119
+ if (tag === "link") {
120
+ const rel = evalHeadValue(desc.props && desc.props.rel);
121
+ return {
122
+ resource: RESOURCE_LINK_RELS.has(rel),
123
+ rel
124
+ };
125
+ }
126
+ if (tag === "style") return {
127
+ resource: !!(desc.props && "href" in desc.props)
128
+ };
129
+ if (tag === "script") return {
130
+ resource: !!(desc.props && "src" in desc.props)
131
+ };
132
+ return {
133
+ resource: false
134
+ };
135
+ }
136
+ function resourceIdentity(tag, props) {
137
+ let id = "res:" + tag + ":" + (props.rel || "") + ":" + (props.href || props.src || "");
138
+ for (let i = 0; i < RESOURCE_QUALIFIERS.length; i++) {
139
+ const q = RESOURCE_QUALIFIERS[i];
140
+ if (props[q] != null) id += ":" + q + "=" + props[q];
141
+ }
142
+ return id;
143
+ }
144
+ function replaceableIdentity(tag, props, key, unique) {
145
+ if (tag === "title") return "title";
146
+ if (tag === "base") return "base";
147
+ if (tag === "meta" && props.charset != null) return "charset";
148
+ if (key != null) return tag + ":key:" + key;
149
+ if (tag === "meta") {
150
+ if (props.name != null) return "meta:name:" + props.name;
151
+ if (props.property != null) return "meta:property:" + props.property;
152
+ if (props["http-equiv"] != null) return "meta:http-equiv:" + props["http-equiv"];
153
+ return unique;
154
+ }
155
+ if (tag === "link") return "link:" + (props.rel || "") + ":" + (props.href || "");
156
+ return unique;
157
+ }
158
+ function resolveHead(groups) {
159
+ const winners = new Map();
160
+ const sorted = groups.slice().sort((a, b) => a.seq - b.seq);
161
+ for (let i = 0; i < sorted.length; i++) {
162
+ const group = sorted[i];
163
+ const byIdentity = new Map();
164
+ for (let j = 0; j < group.tags.length; j++) {
165
+ const t = group.tags[j];
166
+ let list = byIdentity.get(t.identity);
167
+ if (!list) byIdentity.set(t.identity, list = []);
168
+ list.push(t);
169
+ }
170
+ for (const [identity, tags] of byIdentity) {
171
+ if (identity === "title") {
172
+ if (tags.length > 1) console.warn("Multiple <title> tags in one head group; the last one wins.");
173
+ winners.set(identity, {
174
+ seq: group.seq,
175
+ tags: [tags[tags.length - 1]]
176
+ });
177
+ } else {
178
+ winners.set(identity, {
179
+ seq: group.seq,
180
+ tags
181
+ });
182
+ }
183
+ }
184
+ }
185
+ return winners;
186
+ }
187
+
104
188
  function joinAssetPath(base, file) {
105
189
  if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(file)) return file;
106
190
  if (typeof base !== "string" || !base) base = "/";
@@ -242,14 +326,274 @@ function applyAssetTracking(context, tracking, manifest, noScripts) {
242
326
  context.resolveAssetsSync = resolve;
243
327
  }
244
328
  }
329
+ function isCssUrl(url) {
330
+ const q = url.search(/[?#]/);
331
+ return (q === -1 ? url : url.slice(0, q)).endsWith(".css");
332
+ }
333
+ function createHeadRegistry() {
334
+ return {
335
+ pending: [],
336
+ committed: [],
337
+ seq: 0,
338
+ uniq: 0,
339
+ resources: new Set(),
340
+ eagerHtml: "",
341
+ flushed: null,
342
+ shellFlushed: false
343
+ };
344
+ }
345
+ function registerHeadTags(registry, context, tracking, emitResource, nonce, tags) {
346
+ const boundary = context._currentBoundaryId || "";
347
+ let replaceable = null;
348
+ for (let i = 0; i < tags.length; i++) {
349
+ const desc = tags[i];
350
+ if (!desc || !HEAD_ELIGIBLE_TAGS.has(desc.tag)) {
351
+ console.warn(`useHead: ignoring non-head tag`, desc);
352
+ continue;
353
+ }
354
+ const cls = classifyHeadTag(desc);
355
+ if (cls.resource) {
356
+ emitHeadResource(registry, context, tracking, emitResource, nonce, desc, cls.rel);
357
+ } else {
358
+ (replaceable || (replaceable = [])).push(cls.rel !== undefined ? {
359
+ tag: desc.tag,
360
+ props: desc.props,
361
+ key: desc.key,
362
+ rel: cls.rel
363
+ } : desc);
364
+ }
365
+ }
366
+ if (replaceable) registry.pending.push({
367
+ boundary,
368
+ tags: replaceable
369
+ });
370
+ }
371
+ function emitHeadResource(registry, context, tracking, emitResource, nonce, desc, rel) {
372
+ let props;
373
+ try {
374
+ props = evalHeadProps(desc.props || {}, rel !== undefined ? {
375
+ rel
376
+ } : undefined);
377
+ } catch (err) {
378
+ console.warn(`useHead: error evaluating resource tag props`, err);
379
+ return;
380
+ }
381
+ const identity = resourceIdentity(desc.tag, props);
382
+ if (registry.resources.has(identity)) return;
383
+ registry.resources.add(identity);
384
+ if (desc.tag === "link" && (rel === "stylesheet" || rel === "modulepreload")) {
385
+ let plain = true;
386
+ let gateable = rel === "stylesheet";
387
+ for (const name in props) {
388
+ if (name === "rel" || name === "href") continue;
389
+ plain = false;
390
+ if (!STYLESHEET_FETCH_META.has(name)) gateable = false;
391
+ }
392
+ if (plain && props.href != null) {
393
+ const isCss = isCssUrl(props.href);
394
+ if (rel === "stylesheet" ? isCss : !isCss) {
395
+ context.registerAsset(rel === "stylesheet" ? "style" : "module", props.href);
396
+ return;
397
+ }
398
+ }
399
+ if (gateable && props.href != null) {
400
+ const attrHtml = renderHeadAttrHtml(props);
401
+ const entry = {
402
+ href: props.href,
403
+ attrHtml,
404
+ attrs: headAttrRecord(props)
405
+ };
406
+ if (tracking.currentBoundaryId) {
407
+ let styles = tracking.boundaryStyles.get(tracking.currentBoundaryId);
408
+ if (!styles) tracking.boundaryStyles.set(tracking.currentBoundaryId, styles = new Set());
409
+ styles.add(entry);
410
+ }
411
+ const markup = `<link${attrHtml}>`;
412
+ if (emitResource) emitResource(markup, entry);else {
413
+ registry.eagerHtml += markup;
414
+ entry.emitted = true;
415
+ }
416
+ return;
417
+ }
418
+ }
419
+ const url = props.href || props.src;
420
+ if (url != null && tracking.emittedAssets.has(url)) return;
421
+ const markup = renderHeadTagMarkup(desc.tag, props, null, nonce);
422
+ if (emitResource) emitResource(markup);else registry.eagerHtml += markup;
423
+ }
424
+ function commitHeadBoundary(registry, boundary, isPendingFragment) {
425
+ const keep = [];
426
+ const groups = [];
427
+ for (let i = 0; i < registry.pending.length; i++) {
428
+ const reg = registry.pending[i];
429
+ const mine = boundary === "" ? !(isPendingFragment && reg.boundary !== "" && isPendingFragment(reg.boundary)) : reg.boundary === boundary;
430
+ if (!mine) {
431
+ keep.push(reg);
432
+ continue;
433
+ }
434
+ const tags = [];
435
+ for (let j = 0; j < reg.tags.length; j++) {
436
+ const desc = reg.tags[j];
437
+ let props, key;
438
+ try {
439
+ props = evalHeadProps(desc.props || {}, desc.rel !== undefined ? {
440
+ rel: desc.rel
441
+ } : undefined);
442
+ key = evalHeadValue(desc.key);
443
+ } catch (err) {
444
+ console.warn(`useHead: error evaluating tag props`, err);
445
+ continue;
446
+ }
447
+ const identity = replaceableIdentity(desc.tag, props, key, "u:" + registry.uniq++);
448
+ if ((identity === "base" || identity === "charset") && registry.shellFlushed) {
449
+ console.warn(`useHead: <${desc.tag}> (${identity}) registered after shell flush is ignored`);
450
+ continue;
451
+ }
452
+ tags.push({
453
+ tag: desc.tag,
454
+ props,
455
+ identity
456
+ });
457
+ }
458
+ if (tags.length) groups.push({
459
+ seq: registry.seq++,
460
+ tags
461
+ });
462
+ }
463
+ registry.pending = keep;
464
+ for (let i = 0; i < groups.length; i++) registry.committed.push(groups[i]);
465
+ return groups;
466
+ }
467
+ function adoptHeadBoundary(registry, childKey, parentKey) {
468
+ for (let i = 0; i < registry.pending.length; i++) {
469
+ if (registry.pending[i].boundary === childKey) registry.pending[i].boundary = parentKey;
470
+ }
471
+ }
472
+ function dropHeadBoundary(registry, boundary) {
473
+ registry.pending = registry.pending.filter(reg => reg.boundary !== boundary);
474
+ }
475
+ function headGroupSignature(winner) {
476
+ let sig = "" + winner.seq;
477
+ for (let i = 0; i < winner.tags.length; i++) {
478
+ const t = winner.tags[i];
479
+ sig += "|" + t.tag + JSON.stringify(t.props);
480
+ }
481
+ return sig;
482
+ }
483
+ function renderShellHead(registry, nonce, isPendingFragment) {
484
+ commitHeadBoundary(registry, "", isPendingFragment);
485
+ registry.shellFlushed = true;
486
+ const winners = resolveHead(registry.committed);
487
+ registry.flushed = new Map();
488
+ let prelude = "";
489
+ let links = "";
490
+ let metas = "";
491
+ let others = "";
492
+ let scripts = "";
493
+ for (const [identity, winner] of winners) {
494
+ registry.flushed.set(identity, headGroupSignature(winner));
495
+ for (let i = 0; i < winner.tags.length; i++) {
496
+ const t = winner.tags[i];
497
+ const markup = renderHeadTagMarkup(t.tag, t.props, identity, nonce);
498
+ 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;
499
+ }
500
+ }
501
+ return {
502
+ prelude,
503
+ html: registry.eagerHtml + links + metas + others + scripts
504
+ };
505
+ }
506
+ function flushHeadFragment(registry, boundary) {
507
+ const groups = commitHeadBoundary(registry, boundary);
508
+ if (!groups.length) return null;
509
+ const winners = resolveHead(registry.committed);
510
+ const affected = new Set();
511
+ 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);
512
+ const ops = [];
513
+ for (const identity of affected) {
514
+ const winner = winners.get(identity);
515
+ const sig = headGroupSignature(winner);
516
+ if (registry.flushed.get(identity) === sig) continue;
517
+ const existed = registry.flushed.has(identity);
518
+ registry.flushed.set(identity, sig);
519
+ if (identity === "title") {
520
+ const children = winner.tags[0].props.children;
521
+ ops.push(["t", children == null ? "" : String(children)]);
522
+ continue;
523
+ }
524
+ if (existed) ops.push(["r", identity]);
525
+ for (let i = 0; i < winner.tags.length; i++) {
526
+ const t = winner.tags[i];
527
+ const attrs = {};
528
+ for (const name in t.props) {
529
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
530
+ if (!HEAD_ATTR_NAME.test(name)) {
531
+ console.warn(`useHead: ignoring invalid attribute name "${name}"`);
532
+ continue;
533
+ }
534
+ const v = t.props[name];
535
+ if (v == null || v === false) continue;
536
+ attrs[name] = v === true ? "" : String(v);
537
+ }
538
+ const children = t.props.children;
539
+ ops.push(["a", identity, t.tag, attrs, children == null ? null : String(children)]);
540
+ }
541
+ }
542
+ return ops.length ? ops : null;
543
+ }
544
+ function renderHeadAttrHtml(props) {
545
+ let attrs = "";
546
+ for (const name in props) {
547
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
548
+ if (!HEAD_ATTR_NAME.test(name)) {
549
+ console.warn(`useHead: ignoring invalid attribute name "${name}"`);
550
+ continue;
551
+ }
552
+ const v = props[name];
553
+ if (v == null || v === false) continue;
554
+ attrs += v === true ? ` ${name}` : ` ${name}="${escape(String(v), true)}"`;
555
+ }
556
+ return attrs;
557
+ }
558
+ function headAttrRecord(props, skipRelHref) {
559
+ let attrs = null;
560
+ for (const name in props) {
561
+ if (name === "children" || name === "ref" || name.slice(0, 2) === "on") continue;
562
+ if ((name === "rel" || name === "href")) continue;
563
+ if (!HEAD_ATTR_NAME.test(name)) continue;
564
+ const v = props[name];
565
+ if (v == null || v === false) continue;
566
+ (attrs || (attrs = {}))[name] = v === true ? "" : String(v);
567
+ }
568
+ return attrs;
569
+ }
570
+ function renderHeadTagMarkup(tag, props, identity, nonce) {
571
+ let attrs = renderHeadAttrHtml(props);
572
+ if (identity != null) attrs += ` data-dh="${escape(identity, true)}"`;
573
+ if (nonce && (tag === "script" || tag === "style")) attrs += ` nonce="${nonce}"`;
574
+ if (tag === "meta" || tag === "link" || tag === "base") return `<${tag}${attrs}>`;
575
+ let body = props.children == null ? "" : String(props.children);
576
+ if (tag === "script") body = body.replace(/<\/(script)/gi, "<\\/$1");else if (tag === "style") body = escapeStyleContent(body);else body = escape(body);
577
+ return `<${tag}${attrs}>${body}</${tag}>`;
578
+ }
579
+ function useHead(tags) {
580
+ const ctx = solidJs.sharedConfig.context;
581
+ if (!ctx || !ctx.registerHeadTags) {
582
+ console.warn("useHead() called outside of a server render; registration ignored.");
583
+ return;
584
+ }
585
+ ctx.registerHeadTags(Array.isArray(tags) ? tags : [tags]);
586
+ }
245
587
  const VOID_ELEMENTS = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
246
- 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])}`;
588
+ 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])}`;
589
+ 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)}`;
247
590
  function renderToString(code, options = {}) {
248
591
  const {
249
592
  renderId = "",
250
593
  nonce,
251
594
  noScripts,
252
- manifest
595
+ manifest,
596
+ onHead
253
597
  } = options;
254
598
  let scripts = "";
255
599
  const serializer = createHydrationSerializer({
@@ -265,12 +609,16 @@ function renderToString(code, options = {}) {
265
609
  onError: options.onError
266
610
  });
267
611
  const tracking = createAssetTracking();
612
+ const headRegistry = createHeadRegistry();
268
613
  solidJs.sharedConfig.context = {
269
614
  assets: [],
270
615
  nonce,
271
616
  escape: escape,
272
617
  resolve: resolveSSRNode,
273
618
  ssr: ssr,
619
+ registerHeadTags(tags) {
620
+ registerHeadTags(headRegistry, solidJs.sharedConfig.context, tracking, null, nonce, tags);
621
+ },
274
622
  serialize(id, p) {
275
623
  if (solidJs.sharedConfig.context.noHydrate) return;
276
624
  if (p != null && typeof p === "object" && (typeof p.then === "function" || typeof p[Symbol.asyncIterator] === "function")) {
@@ -306,7 +654,8 @@ function renderToString(code, options = {}) {
306
654
  solidJs.sharedConfig.context.noHydrate = true;
307
655
  serializer.close();
308
656
  const assetsHtml = resolveAssetsHtml(solidJs.sharedConfig.context.assets);
309
- return assembleDocument(html, assetsHtml, tracking.emittedAssets, tracking.inlineStyles, scripts.length ? scripts : "", nonce);
657
+ const head = renderShellHead(headRegistry, nonce, null);
658
+ return assembleDocument(html, assetsHtml, tracking.emittedAssets, tracking.inlineStyles, scripts.length ? scripts : "", nonce, head, onHead);
310
659
  }
311
660
  function renderToStream(code, options = {}) {
312
661
  let {
@@ -315,7 +664,8 @@ function renderToStream(code, options = {}) {
315
664
  onCompleteAll,
316
665
  renderId = "",
317
666
  noScripts,
318
- manifest
667
+ manifest,
668
+ onHead
319
669
  } = options;
320
670
  let dispose;
321
671
  const blockingPromises = new Set();
@@ -357,8 +707,8 @@ function renderToStream(code, options = {}) {
357
707
  if (styles.links.length) {
358
708
  emitTask(`$dfs("${key}",${styles.links.length},${deferActivation ? 1 : 0})`);
359
709
  writeTasks();
360
- for (const url of styles.links) {
361
- buffer.write(`<link rel="stylesheet" href="${url}" onload="$dfc('${key}')" onerror="$dfc('${key}')">`);
710
+ for (const entry of styles.links) {
711
+ 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}')">`);
362
712
  }
363
713
  buffer.write(`<template id="${key}">${value}</template>`);
364
714
  } else {
@@ -376,10 +726,12 @@ function renderToStream(code, options = {}) {
376
726
  buffer.write(`<link rel="modulepreload" href="${value}">`);
377
727
  } else if (type === "inline-style") {
378
728
  buffer.write(renderInlineStyle(value, nonce));
729
+ } else if (type === "head-tag") {
730
+ buffer.write(value);
379
731
  }
380
732
  },
381
733
  shell(shellHtml, meta) {
382
- buffer.write(assembleDocument(shellHtml, meta.assets, meta.preloads, meta.inlineStyles, meta.tasks.length ? meta.tasks : "", nonce));
734
+ buffer.write(assembleDocument(shellHtml, meta.assets, meta.preloads, meta.inlineStyles, meta.tasks.length ? meta.tasks : "", nonce, meta.head, onHead));
383
735
  },
384
736
  ...options.sink
385
737
  };
@@ -450,10 +802,28 @@ function renderToStream(code, options = {}) {
450
802
  }
451
803
  };
452
804
  const tracking = createAssetTracking();
805
+ const headRegistry = createHeadRegistry();
806
+ let headScriptFlushed = false;
807
+ const emitHeadOps = (key, ops) => {
808
+ const payload = JSON.stringify(ops).replace(/</g, "\\u003C");
809
+ emitTask(`${!headScriptFlushed ? HEAD_SCRIPT : ""}(_$HY.hp=_$HY.hp||{})[${JSON.stringify(key)}]=${payload}`);
810
+ headScriptFlushed = true;
811
+ };
453
812
  solidJs.sharedConfig.context = context = {
454
813
  async: true,
455
814
  assets: [],
456
815
  nonce,
816
+ registerHeadTags(tags) {
817
+ registerHeadTags(headRegistry, context, tracking,
818
+ (markup, gateEntry) => {
819
+ if (!firstFlushed) {
820
+ headRegistry.eagerHtml += markup;
821
+ if (gateEntry) gateEntry.emitted = true;
822
+ } else if (!gateEntry || !tracking.currentBoundaryId) {
823
+ sink.asset("head-tag", markup);
824
+ }
825
+ }, nonce, tags);
826
+ },
457
827
  registerAsset(type, value) {
458
828
  if (type === "inline-style") {
459
829
  const entry = tracking.registerInlineStyle(value);
@@ -544,10 +914,12 @@ function renderToStream(code, options = {}) {
544
914
  parent.children[key] = value !== undefined ? value : "";
545
915
  serializeFragmentAssets(key, tracking.boundaryModules, context);
546
916
  propagateBoundaryStyles(key, parentKey, tracking);
917
+ adoptHeadBoundary(headRegistry, key, parentKey);
547
918
  item.resolve();
548
919
  return;
549
920
  }
550
921
  if (!completed) {
922
+ if (error) dropHeadBoundary(headRegistry, key);
551
923
  if (!firstFlushed) {
552
924
  queue(() => html = replacePlaceholder(html, key, value !== undefined ? value : ""));
553
925
  serializeFragmentAssets(key, tracking.boundaryModules, context);
@@ -555,6 +927,8 @@ function renderToStream(code, options = {}) {
555
927
  } else {
556
928
  serializeFragmentAssets(key, tracking.boundaryModules, context);
557
929
  const styles = collectStreamStyles(key, tracking, headStyles);
930
+ const headOps = error ? null : flushHeadFragment(headRegistry, key);
931
+ if (headOps) emitHeadOps(key, headOps);
558
932
  sink.fragment(key, value !== undefined ? value : " ", {
559
933
  styles,
560
934
  revealGroup,
@@ -642,14 +1016,16 @@ function renderToStream(code, options = {}) {
642
1016
  const assetsHtml = resolveAssetsHtml(context.assets);
643
1017
  headStyles = new Set();
644
1018
  for (const url of tracking.emittedAssets) {
645
- if (url.endsWith(".css")) headStyles.add(url);
1019
+ if (isCssUrl(url)) headStyles.add(url);
646
1020
  }
647
1021
  serializeRootAssets();
1022
+ const head = renderShellHead(headRegistry, nonce, k => registry.has(k));
648
1023
  sink.shell(html, {
649
1024
  assets: assetsHtml,
650
1025
  preloads: tracking.emittedAssets,
651
1026
  inlineStyles: tracking.inlineStyles,
652
- tasks
1027
+ tasks,
1028
+ head
653
1029
  });
654
1030
  tasks = "";
655
1031
  onCompleteShell && onCompleteShell({
@@ -1149,23 +1525,42 @@ function resolveAssetsHtml(assets) {
1149
1525
  for (let i = 0, len = assets.length; i < len; i++) out += assets[i]();
1150
1526
  return out;
1151
1527
  }
1152
- function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts, nonce) {
1528
+ function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts, nonce, headTags, onHead) {
1153
1529
  const scriptTag = scripts ? `<script${nonce ? ` nonce="${nonce}"` : ""}>${scripts}</script>` : "";
1154
- if (!assetsHtml && !(emittedAssets && emittedAssets.size) && !(inlineStyles && inlineStyles.size)) {
1530
+ const headTagsHtml = headTags ? headTags.html : "";
1531
+ const headPrelude = headTags ? headTags.prelude : "";
1532
+ if (!onHead && !assetsHtml && !headTagsHtml && !headPrelude && !(emittedAssets && emittedAssets.size) && !(inlineStyles && inlineStyles.size)) {
1155
1533
  if (!scriptTag) return html;
1156
1534
  const xs = html.indexOf("<!--xs-->");
1157
1535
  return xs === -1 ? html + scriptTag : html.slice(0, xs) + scriptTag + html.slice(xs);
1158
1536
  }
1537
+ if (headPrelude) {
1538
+ const open = html.match(/<head(?:\s[^>]*)?>/);
1539
+ if (open) {
1540
+ const at = open.index + open[0].length;
1541
+ html = html.slice(0, at) + headPrelude + html.slice(at);
1542
+ }
1543
+ }
1159
1544
  const headIdx = html.indexOf("</head>");
1160
1545
  if (headIdx === -1) {
1546
+ if (onHead) {
1547
+ onHead(headPrelude + headTagsHtml + (assetsHtml || "") + renderHeadAssets(emittedAssets, inlineStyles, nonce));
1548
+ }
1161
1549
  if (!scriptTag) return html;
1162
1550
  const xs = html.indexOf("<!--xs-->");
1163
1551
  return xs === -1 ? html + scriptTag : html.slice(0, xs) + scriptTag + html.slice(xs);
1164
1552
  }
1165
- let head = assetsHtml || "";
1553
+ const head = headTagsHtml + (assetsHtml || "") + renderHeadAssets(emittedAssets, inlineStyles, nonce);
1554
+ if (!scriptTag) return html.slice(0, headIdx) + head + html.slice(headIdx);
1555
+ const xsIdx = html.indexOf("<!--xs-->");
1556
+ if (xsIdx === -1) return html.slice(0, headIdx) + head + html.slice(headIdx) + scriptTag;
1557
+ 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);
1558
+ }
1559
+ function renderHeadAssets(emittedAssets, inlineStyles, nonce) {
1560
+ let head = "";
1166
1561
  if (emittedAssets && emittedAssets.size) {
1167
1562
  for (const url of emittedAssets) {
1168
- head += url.endsWith(".css") ? `<link rel="stylesheet" href="${url}">` : `<link rel="modulepreload" href="${url}">`;
1563
+ head += isCssUrl(url) ? `<link rel="stylesheet" href="${url}">` : `<link rel="modulepreload" href="${url}">`;
1169
1564
  }
1170
1565
  }
1171
1566
  if (inlineStyles && inlineStyles.size) {
@@ -1175,10 +1570,7 @@ function assembleDocument(html, assetsHtml, emittedAssets, inlineStyles, scripts
1175
1570
  head += renderInlineStyle(entry, nonce);
1176
1571
  }
1177
1572
  }
1178
- if (!scriptTag) return html.slice(0, headIdx) + head + html.slice(headIdx);
1179
- const xsIdx = html.indexOf("<!--xs-->");
1180
- if (xsIdx === -1) return html.slice(0, headIdx) + head + html.slice(headIdx) + scriptTag;
1181
- 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);
1573
+ return head;
1182
1574
  }
1183
1575
  function serializeFragmentAssets(key, boundaryModules, context) {
1184
1576
  const map = boundaryModules.get(key);
@@ -1208,7 +1600,12 @@ function collectStreamStyles(key, tracking, headStyles) {
1208
1600
  for (const entry of styles) {
1209
1601
  if (typeof entry === "string") {
1210
1602
  if (!headStyles || !headStyles.has(entry)) links.push(entry);
1211
- } else if (!entry.emitted) {
1603
+ } else if (entry.emitted) {
1604
+ continue;
1605
+ } else if (entry.attrHtml !== undefined) {
1606
+ entry.emitted = true;
1607
+ links.push(entry);
1608
+ } else {
1212
1609
  entry.emitted = true;
1213
1610
  inline.push(entry);
1214
1611
  }
@@ -1468,8 +1865,41 @@ function Portal(props) {
1468
1865
  if (o?.id != null) solidJs.getNextChildId(o);
1469
1866
  return undefined;
1470
1867
  }
1471
- function clientOnly(_fn, _options = {}) {
1472
- return props => solidJs.createMemo(() => props.fallback);
1868
+ function registerClientOnlyPreload(moduleUrl) {
1869
+ const ctx = solidJs.sharedConfig.context;
1870
+ if (!ctx?.registerAsset || !ctx.resolveAssets) return;
1871
+ const registerAsset = ctx.registerAsset;
1872
+ const resolve = ctx.resolveAssets;
1873
+ const apply = assets => {
1874
+ if (!assets) return;
1875
+ for (let i = 0; i < assets.css.length; i++) {
1876
+ const css = assets.css[i];
1877
+ if (typeof css === "string") registerAsset("style", css);else registerAsset("inline-style", css);
1878
+ }
1879
+ for (let i = 0; i < assets.js.length; i++) registerAsset("module", assets.js[i]);
1880
+ };
1881
+ const assets = resolve(moduleUrl);
1882
+ if (assets && typeof assets.then === "function") {
1883
+ const boundary = ctx._currentBoundaryId;
1884
+ assets.then(resolved => {
1885
+ const current = ctx._currentBoundaryId;
1886
+ ctx._currentBoundaryId = boundary;
1887
+ try {
1888
+ apply(resolved);
1889
+ } finally {
1890
+ ctx._currentBoundaryId = current;
1891
+ }
1892
+ },
1893
+ () => {});
1894
+ } else {
1895
+ apply(assets);
1896
+ }
1897
+ }
1898
+ function clientOnly(_fn, _options = {}, moduleUrl) {
1899
+ return props => {
1900
+ if (moduleUrl) registerClientOnlyPreload(moduleUrl);
1901
+ return solidJs.createMemo(() => props.fallback);
1902
+ };
1473
1903
  }
1474
1904
  function httpStatus(code, text) {
1475
1905
  const event = getRequestEvent();
@@ -1637,3 +2067,4 @@ exports.template = notSup;
1637
2067
  exports.unregisterDelegatedContainer = notSup;
1638
2068
  exports.unregisterDelegatedRoot = notSup;
1639
2069
  exports.useAssets = useAssets;
2070
+ exports.useHead = useHead;