@zubra/runtime 5.10.2 → 5.11.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.
@@ -635,19 +635,26 @@ const loadRemark = () => (remarking ??= import('./remark.js').then(
635
635
  (module) => (remarker = module),
636
636
  () => (remarker = { remark: () => false }),
637
637
  ));
638
- /** Whether `root`'s server markup lost the comments `template` has (its top-level elements are there). */
639
- function lostComments(root, template) {
638
+ /**
639
+ * Whether `root`'s server markup lost the comments `template` has (its top-level elements are
640
+ * there). A template that is only regions has none of its own: `roots`, from the compiler, names
641
+ * the elements its regions' blocks start with, and markup with none of them is the page's own
642
+ * content (text a framework gave it, say), which needs no check, so the element renders now.
643
+ */
644
+ function lostComments(root, template, roots) {
640
645
  if (!root || !hasContent(root) || !hasComment(template) || hasComment(root)) return false;
641
646
  // The page's own children for a slot aren't server markup: the template's top-level elements
642
647
  // must be there, in order.
643
648
  const children = [...root.children];
644
649
  let at = 0;
650
+ let elements = 0;
645
651
  for (const node of positions(template.firstChild, null)) {
646
652
  if (node.nodeType !== 1) continue;
647
653
  while (at < children.length && children[at].localName !== node.localName) at++;
648
654
  if (at++ >= children.length) return false;
655
+ elements++;
649
656
  }
650
- return true;
657
+ return elements || !roots ? true : children.some((child) => roots.includes(child.localName));
651
658
  }
652
659
 
653
660
  /** A range's shape: its top-level positions as tag names and region markers. */
@@ -1602,7 +1609,7 @@ export function $define(tag, definition) {
1602
1609
 
1603
1610
  function defineElement(tag, definition) {
1604
1611
  const { props = {}, shadow = false, form = false, events = [], styled = false, inPage = false } = definition;
1605
- let { template = '', slots = [], styles = null, setup } = definition;
1612
+ let { roots, template = '', slots = [], styles = null, setup } = definition;
1606
1613
  // On a server (no DOM), a stand-in with the same `zubra` description, so a module that imports a
1607
1614
  // component loads there: adapters render the host tag, and `@zubra/runtime/server` renders it.
1608
1615
  if (typeof HTMLElement === 'undefined') return serverElement(tag, { props, shadow, form, slots, events, styled });
@@ -1699,7 +1706,7 @@ function defineElement(tag, definition) {
1699
1706
  if (root.adoptedStyleSheets && !(inPage && root === document)) adopt(root, ownSheets());
1700
1707
  }
1701
1708
  if (this.#scope) this.#scope.resume();
1702
- else if (!remarker && lostComments(shadow ? this.shadowRoot : this, componentTemplate.content)) {
1709
+ else if (!remarker && lostComments(shadow ? this.shadowRoot : this, componentTemplate.content, roots)) {
1703
1710
  // Server markup without its comments: start once the code that puts them back is here.
1704
1711
  this.#starting ??= loadRemark().then(() => {
1705
1712
  this.#starting = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zubra/runtime",
3
- "version": "5.10.2",
3
+ "version": "5.11.0",
4
4
  "description": "Browser runtime for Zubra compiled components, built on the TC39 Signals API",
5
5
  "license": "ISC",
6
6
  "repository": {
package/src/element.js CHANGED
@@ -654,19 +654,26 @@ const loadRemark = () => (remarking ??= import('./remark.js').then(
654
654
  (module) => (remarker = module),
655
655
  () => (remarker = { remark: () => false }),
656
656
  ));
657
- /** Whether `root`'s server markup lost the comments `template` has (its top-level elements are there). */
658
- function lostComments(root, template) {
657
+ /**
658
+ * Whether `root`'s server markup lost the comments `template` has (its top-level elements are
659
+ * there). A template that is only regions has none of its own: `roots`, from the compiler, names
660
+ * the elements its regions' blocks start with, and markup with none of them is the page's own
661
+ * content (text a framework gave it, say), which needs no check, so the element renders now.
662
+ */
663
+ function lostComments(root, template, roots) {
659
664
  if (!root || !hasContent(root) || !hasComment(template) || hasComment(root)) return false;
660
665
  // The page's own children for a slot aren't server markup: the template's top-level elements
661
666
  // must be there, in order.
662
667
  const children = [...root.children];
663
668
  let at = 0;
669
+ let elements = 0;
664
670
  for (const node of positions(template.firstChild, null)) {
665
671
  if (node.nodeType !== 1) continue;
666
672
  while (at < children.length && children[at].localName !== node.localName) at++;
667
673
  if (at++ >= children.length) return false;
674
+ elements++;
668
675
  }
669
- return true;
676
+ return elements || !roots ? true : children.some((child) => roots.includes(child.localName));
670
677
  }
671
678
 
672
679
  /** A range's shape: its top-level positions as tag names and region markers. */
@@ -1635,7 +1642,7 @@ const HOT = Symbol('zubra.hot');
1635
1642
 
1636
1643
  function defineElement(tag, definition) {
1637
1644
  const { props = {}, shadow = false, form = false, events = [], styled = false, inPage = false } = definition;
1638
- let { template = '', slots = [], styles = null, setup } = definition;
1645
+ let { roots, template = '', slots = [], styles = null, setup } = definition;
1639
1646
  // On a server (no DOM), a stand-in with the same `zubra` description, so a module that imports a
1640
1647
  // component loads there: adapters render the host tag, and `@zubra/runtime/server` renders it.
1641
1648
  if (typeof HTMLElement === 'undefined') return serverElement(tag, { props, shadow, form, slots, events, styled });
@@ -1680,7 +1687,7 @@ function defineElement(tag, definition) {
1680
1687
  /** Swap in a new version of the component (see above); false if its interface changed. */
1681
1688
  static [HOT](next) {
1682
1689
  if (shape(next) !== shape(definition)) return false;
1683
- ({ template = '', slots = [], setup } = next);
1690
+ ({ roots, template = '', slots = [], setup } = next);
1684
1691
  componentTemplate.content = blueprint({ template });
1685
1692
  templateSignature = signature(childRange(componentTemplate.content));
1686
1693
  const nextStyles = next.styles ?? null;
@@ -1760,7 +1767,7 @@ function defineElement(tag, definition) {
1760
1767
  if (root.adoptedStyleSheets && !(inPage && root === document)) adopt(root, ownSheets());
1761
1768
  }
1762
1769
  if (this.#scope) this.#scope.resume();
1763
- else if (!remarker && lostComments(shadow ? this.shadowRoot : this, componentTemplate.content)) {
1770
+ else if (!remarker && lostComments(shadow ? this.shadowRoot : this, componentTemplate.content, roots)) {
1764
1771
  // Server markup without its comments: start once the code that puts them back is here.
1765
1772
  this.#starting ??= loadRemark().then(() => {
1766
1773
  this.#starting = null;
package/types/index.d.ts CHANGED
@@ -184,6 +184,8 @@ export interface Definition {
184
184
  form?: boolean;
185
185
  slots?: string[];
186
186
  events?: string[];
187
+ /** For a template that is only regions: the elements their blocks start with (server markup's top level). */
188
+ roots?: string[];
187
189
  /** The component has a `<style>`. */
188
190
  styled?: boolean;
189
191
  /** Its stylesheet, when the module carries it (otherwise the bundler adds it to the page). */