server.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import './server-shim.js';
  2. import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
  3. import * as parse5 from 'parse5';
  4. function isCustomElementTag(name) {
  5. return typeof name === 'string' && /-/.test(name);
  6. }
  7. function getCustomElementConstructor(name) {
  8. if (typeof customElements !== 'undefined' && isCustomElementTag(name)) {
  9. return customElements.get(name) || null;
  10. } else if (typeof name === 'function') {
  11. return name;
  12. }
  13. return null;
  14. }
  15. async function isLitElement(Component) {
  16. const Ctr = getCustomElementConstructor(Component);
  17. return !!Ctr?._$litElement$;
  18. }
  19. async function check(Component) {
  20. // Lit doesn't support getting a tagName from a Constructor at this time.
  21. // So this must be a string at the moment.
  22. return !!(await isLitElement(Component));
  23. }
  24. function* render(Component, attrs, slots) {
  25. let tagName = Component;
  26. if (typeof tagName !== 'string') {
  27. tagName = Component[Symbol.for('tagName')];
  28. }
  29. const instance = new LitElementRenderer(tagName);
  30. // LitElementRenderer creates a new element instance, so copy over.
  31. const Ctr = getCustomElementConstructor(tagName);
  32. let shouldDeferHydration = false;
  33. if (attrs) {
  34. for (let [name, value] of Object.entries(attrs)) {
  35. const isReactiveProperty = name in Ctr.prototype;
  36. const isReflectedReactiveProperty = Ctr.elementProperties.get(name)?.reflect;
  37. // Only defer hydration if we are setting a reactive property that cannot
  38. // be reflected / serialized as a property.
  39. shouldDeferHydration ||= isReactiveProperty && !isReflectedReactiveProperty;
  40. if (isReactiveProperty) {
  41. instance.setProperty(name, value);
  42. } else {
  43. instance.setAttribute(name, value);
  44. }
  45. }
  46. }
  47. instance.connectedCallback();
  48. yield `<${tagName}${shouldDeferHydration ? ' defer-hydration' : ''}`;
  49. yield* instance.renderAttributes();
  50. yield `>`;
  51. const shadowContents = instance.renderShadow({
  52. elementRenderers: [LitElementRenderer],
  53. customElementInstanceStack: [instance],
  54. customElementHostStack: [instance],
  55. deferHydration: false,
  56. });
  57. if (shadowContents !== undefined) {
  58. const { mode = 'open', delegatesFocus } = instance.shadowRootOptions ?? {};
  59. // `delegatesFocus` is intentionally allowed to coerce to boolean to
  60. // match web platform behavior.
  61. const delegatesfocusAttr = delegatesFocus ? ' shadowrootdelegatesfocus' : '';
  62. yield `<template shadowroot="${mode}" shadowrootmode="${mode}"${delegatesfocusAttr}>`;
  63. yield* shadowContents;
  64. yield '</template>';
  65. }
  66. if (slots) {
  67. for (let [slot, value = ''] of Object.entries(slots)) {
  68. if (slot !== 'default' && value) {
  69. // Parse the value as a concatenated string
  70. const fragment = parse5.parseFragment(`${value}`);
  71. // Add the missing slot attribute to child Element nodes
  72. for (const node of fragment.childNodes) {
  73. if (node.tagName && !node.attrs.some(({ name }) => name === 'slot')) {
  74. node.attrs.push({ name: 'slot', value: slot });
  75. }
  76. }
  77. value = parse5.serialize(fragment);
  78. }
  79. yield value;
  80. }
  81. }
  82. yield `</${tagName}>`;
  83. }
  84. async function renderToStaticMarkup(Component, props, slots) {
  85. let tagName = Component;
  86. let out = '';
  87. for (let chunk of render(tagName, props, slots)) {
  88. out += chunk;
  89. }
  90. return {
  91. html: out,
  92. };
  93. }
  94. export default {
  95. check,
  96. renderToStaticMarkup,
  97. };