experimental-hydrate.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. /**
  2. * @license
  3. * Copyright 2019 Google LLC
  4. * SPDX-License-Identifier: BSD-3-Clause
  5. */
  6. import { noChange, _$LH } from './lit-html.js';
  7. import { PartType } from './directive.js';
  8. import { isPrimitive, isSingleExpression, isTemplateResult, } from './directive-helpers.js';
  9. const { _TemplateInstance: TemplateInstance, _isIterable: isIterable, _resolveDirective: resolveDirective, _ChildPart: ChildPart, _ElementPart: ElementPart, } = _$LH;
  10. /**
  11. * hydrate() operates on a container with server-side rendered content and
  12. * restores the client side data structures needed for lit-html updates such as
  13. * TemplateInstances and Parts. After calling `hydrate`, lit-html will behave as
  14. * if it initially rendered the DOM, and any subsequent updates will update
  15. * efficiently, the same as if lit-html had rendered the DOM on the client.
  16. *
  17. * hydrate() must be called on DOM that adheres the to lit-ssr structure for
  18. * parts. ChildParts must be represented with both a start and end comment
  19. * marker, and ChildParts that contain a TemplateInstance must have the template
  20. * digest written into the comment data.
  21. *
  22. * Since render() encloses its output in a ChildPart, there must always be a root
  23. * ChildPart.
  24. *
  25. * Example (using for # ... for annotations in HTML)
  26. *
  27. * Given this input:
  28. *
  29. * html`<div class=${x}>${y}</div>`
  30. *
  31. * The SSR DOM is:
  32. *
  33. * <!--lit-part AEmR7W+R0Ak=--> # Start marker for the root ChildPart created
  34. * # by render(). Includes the digest of the
  35. * # template
  36. * <div class="TEST_X">
  37. * <!--lit-node 0--> # Indicates there are attribute bindings here
  38. * # The number is the depth-first index of the parent
  39. * # node in the template.
  40. * <!--lit-part--> # Start marker for the ${x} expression
  41. * TEST_Y
  42. * <!--/lit-part--> # End marker for the ${x} expression
  43. * </div>
  44. *
  45. * <!--/lit-part--> # End marker for the root ChildPart
  46. *
  47. * @param rootValue
  48. * @param container
  49. * @param userOptions
  50. */
  51. export const hydrate = (rootValue, container, options = {}) => {
  52. // TODO(kschaaf): Do we need a helper for _$litPart$ ("part for node")?
  53. // This property needs to remain unminified.
  54. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  55. if (container['_$litPart$'] !== undefined) {
  56. throw new Error('container already contains a live render');
  57. }
  58. // Since render() creates a ChildPart to render into, we'll always have
  59. // exactly one root part. We need to hold a reference to it so we can set
  60. // it in the parts cache.
  61. let rootPart = undefined;
  62. // When we are in-between ChildPart markers, this is the current ChildPart.
  63. // It's needed to be able to set the ChildPart's endNode when we see a
  64. // close marker
  65. let currentChildPart = undefined;
  66. // Used to remember parent template state as we recurse into nested
  67. // templates
  68. const stack = [];
  69. const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT, null, false);
  70. let marker;
  71. // Walk the DOM looking for part marker comments
  72. while ((marker = walker.nextNode()) !== null) {
  73. const markerText = marker.data;
  74. if (markerText.startsWith('lit-part')) {
  75. if (stack.length === 0 && rootPart !== undefined) {
  76. throw new Error('there must be only one root part per container');
  77. }
  78. // Create a new ChildPart and push it onto the stack
  79. currentChildPart = openChildPart(rootValue, marker, stack, options);
  80. rootPart !== null && rootPart !== void 0 ? rootPart : (rootPart = currentChildPart);
  81. }
  82. else if (markerText.startsWith('lit-node')) {
  83. // Create and hydrate attribute parts into the current ChildPart on the
  84. // stack
  85. createAttributeParts(marker, stack, options);
  86. // Remove `defer-hydration` attribute, if any
  87. const parent = marker.parentElement;
  88. if (parent.hasAttribute('defer-hydration')) {
  89. parent.removeAttribute('defer-hydration');
  90. }
  91. }
  92. else if (markerText.startsWith('/lit-part')) {
  93. // Close the current ChildPart, and pop the previous one off the stack
  94. if (stack.length === 1 && currentChildPart !== rootPart) {
  95. throw new Error('internal error');
  96. }
  97. currentChildPart = closeChildPart(marker, currentChildPart, stack);
  98. }
  99. }
  100. console.assert(rootPart !== undefined, 'there should be exactly one root part in a render container');
  101. // This property needs to remain unminified.
  102. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  103. container['_$litPart$'] = rootPart;
  104. };
  105. const openChildPart = (rootValue, marker, stack, options) => {
  106. let value;
  107. // We know the startNode now. We'll know the endNode when we get to
  108. // the matching marker and set it in closeChildPart()
  109. // TODO(kschaaf): Current constructor takes both nodes
  110. let part;
  111. if (stack.length === 0) {
  112. part = new ChildPart(marker, null, undefined, options);
  113. value = rootValue;
  114. }
  115. else {
  116. const state = stack[stack.length - 1];
  117. if (state.type === 'template-instance') {
  118. part = new ChildPart(marker, null, state.instance, options);
  119. state.instance._parts.push(part);
  120. value = state.result.values[state.instancePartIndex++];
  121. state.templatePartIndex++;
  122. }
  123. else if (state.type === 'iterable') {
  124. part = new ChildPart(marker, null, state.part, options);
  125. const result = state.iterator.next();
  126. if (result.done) {
  127. value = undefined;
  128. state.done = true;
  129. throw new Error('Unhandled shorter than expected iterable');
  130. }
  131. else {
  132. value = result.value;
  133. }
  134. state.part._$committedValue.push(part);
  135. }
  136. else {
  137. // state.type === 'leaf'
  138. // TODO(kschaaf): This is unexpected, and likely a result of a primitive
  139. // been rendered on the client when a TemplateResult was rendered on the
  140. // server; this part will be hydrated but not used. We can detect it, but
  141. // we need to decide what to do in this case. Note that this part won't be
  142. // retained by any parent TemplateInstance, since a primitive had been
  143. // rendered in its place.
  144. // https://github.com/lit/lit/issues/1434
  145. // throw new Error('Hydration value mismatch: Found a TemplateInstance' +
  146. // 'where a leaf value was expected');
  147. part = new ChildPart(marker, null, state.part, options);
  148. }
  149. }
  150. // Initialize the ChildPart state depending on the type of value and push
  151. // it onto the stack. This logic closely follows the ChildPart commit()
  152. // cascade order:
  153. // 1. directive
  154. // 2. noChange
  155. // 3. primitive (note strings must be handled before iterables, since they
  156. // are iterable)
  157. // 4. TemplateResult
  158. // 5. Node (not yet implemented, but fallback handling is fine)
  159. // 6. Iterable
  160. // 7. nothing (handled in fallback)
  161. // 8. Fallback for everything else
  162. value = resolveDirective(part, value);
  163. if (value === noChange) {
  164. stack.push({ part, type: 'leaf' });
  165. }
  166. else if (isPrimitive(value)) {
  167. stack.push({ part, type: 'leaf' });
  168. part._$committedValue = value;
  169. // TODO(kschaaf): We can detect when a primitive is being hydrated on the
  170. // client where a TemplateResult was rendered on the server, but we need to
  171. // decide on a strategy for what to do next.
  172. // https://github.com/lit/lit/issues/1434
  173. // if (marker.data !== 'lit-part') {
  174. // throw new Error('Hydration value mismatch: Primitive found where TemplateResult expected');
  175. // }
  176. }
  177. else if (isTemplateResult(value)) {
  178. // Check for a template result digest
  179. const markerWithDigest = `lit-part ${digestForTemplateResult(value)}`;
  180. if (marker.data === markerWithDigest) {
  181. const template = ChildPart.prototype._$getTemplate(value);
  182. const instance = new TemplateInstance(template, part);
  183. stack.push({
  184. type: 'template-instance',
  185. instance,
  186. part,
  187. templatePartIndex: 0,
  188. instancePartIndex: 0,
  189. result: value,
  190. });
  191. // For TemplateResult values, we set the part value to the
  192. // generated TemplateInstance
  193. part._$committedValue = instance;
  194. }
  195. else {
  196. // TODO: if this isn't the server-rendered template, do we
  197. // need to stop hydrating this subtree? Clear it? Add tests.
  198. throw new Error('Hydration value mismatch: Unexpected TemplateResult rendered to part');
  199. }
  200. }
  201. else if (isIterable(value)) {
  202. // currentChildPart.value will contain an array of ChildParts
  203. stack.push({
  204. part: part,
  205. type: 'iterable',
  206. value,
  207. iterator: value[Symbol.iterator](),
  208. done: false,
  209. });
  210. part._$committedValue = [];
  211. }
  212. else {
  213. // Fallback for everything else (nothing, Objects, Functions,
  214. // etc.): we just initialize the part's value
  215. // Note that `Node` value types are not currently supported during
  216. // SSR, so that part of the cascade is missing.
  217. stack.push({ part: part, type: 'leaf' });
  218. part._$committedValue = value == null ? '' : value;
  219. }
  220. return part;
  221. };
  222. const closeChildPart = (marker, part, stack) => {
  223. if (part === undefined) {
  224. throw new Error('unbalanced part marker');
  225. }
  226. part._$endNode = marker;
  227. const currentState = stack.pop();
  228. if (currentState.type === 'iterable') {
  229. if (!currentState.iterator.next().done) {
  230. throw new Error('unexpected longer than expected iterable');
  231. }
  232. }
  233. if (stack.length > 0) {
  234. const state = stack[stack.length - 1];
  235. return state.part;
  236. }
  237. else {
  238. return undefined;
  239. }
  240. };
  241. const createAttributeParts = (comment, stack, options) => {
  242. var _a;
  243. // Get the nodeIndex from DOM. We're only using this for an integrity
  244. // check right now, we might not need it.
  245. const match = /lit-node (\d+)/.exec(comment.data);
  246. const nodeIndex = parseInt(match[1]);
  247. // For void elements, the node the comment was referring to will be
  248. // the previousSibling; for non-void elements, the comment is guaranteed
  249. // to be the first child of the element (i.e. it won't have a previousSibling
  250. // meaning it should use the parentElement)
  251. const node = (_a = comment.previousSibling) !== null && _a !== void 0 ? _a : comment.parentElement;
  252. const state = stack[stack.length - 1];
  253. if (state.type === 'template-instance') {
  254. const instance = state.instance;
  255. // eslint-disable-next-line no-constant-condition
  256. while (true) {
  257. // If the next template part is in attribute-position on the current node,
  258. // create the instance part for it and prime its state
  259. const templatePart = instance._$template.parts[state.templatePartIndex];
  260. if (templatePart === undefined ||
  261. (templatePart.type !== PartType.ATTRIBUTE &&
  262. templatePart.type !== PartType.ELEMENT) ||
  263. templatePart.index !== nodeIndex) {
  264. break;
  265. }
  266. if (templatePart.type === PartType.ATTRIBUTE) {
  267. // The instance part is created based on the constructor saved in the
  268. // template part
  269. const instancePart = new templatePart.ctor(node, templatePart.name, templatePart.strings, state.instance, options);
  270. const value = isSingleExpression(instancePart)
  271. ? state.result.values[state.instancePartIndex]
  272. : state.result.values;
  273. // Setting the attribute value primes committed value with the resolved
  274. // directive value; we only then commit that value for event/property
  275. // parts since those were not serialized, and pass `noCommit` for the
  276. // others to avoid perf impact of touching the DOM unnecessarily
  277. const noCommit = !(instancePart.type === PartType.EVENT ||
  278. instancePart.type === PartType.PROPERTY);
  279. instancePart._$setValue(value, instancePart, state.instancePartIndex, noCommit);
  280. state.instancePartIndex += templatePart.strings.length - 1;
  281. instance._parts.push(instancePart);
  282. }
  283. else {
  284. // templatePart.type === PartType.ELEMENT
  285. const instancePart = new ElementPart(node, state.instance, options);
  286. resolveDirective(instancePart, state.result.values[state.instancePartIndex++]);
  287. instance._parts.push(instancePart);
  288. }
  289. state.templatePartIndex++;
  290. }
  291. }
  292. else {
  293. throw new Error('internal error');
  294. }
  295. };
  296. // Number of 32 bit elements to use to create template digests
  297. const digestSize = 2;
  298. // We need to specify a digest to use across rendering environments. This is a
  299. // simple digest build from a DJB2-ish hash modified from:
  300. // https://github.com/darkskyapp/string-hash/blob/master/index.js
  301. // It has been changed to an array of hashes to add additional bits.
  302. // Goals:
  303. // - Extremely low collision rate. We may not be able to detect collisions.
  304. // - Extremely fast.
  305. // - Extremely small code size.
  306. // - Safe to include in HTML comment text or attribute value.
  307. // - Easily specifiable and implementable in multiple languages.
  308. // We don't care about cryptographic suitability.
  309. export const digestForTemplateResult = (templateResult) => {
  310. const hashes = new Uint32Array(digestSize).fill(5381);
  311. for (const s of templateResult.strings) {
  312. for (let i = 0; i < s.length; i++) {
  313. hashes[i % digestSize] = (hashes[i % digestSize] * 33) ^ s.charCodeAt(i);
  314. }
  315. }
  316. return btoa(String.fromCharCode(...new Uint8Array(hashes.buffer)));
  317. };
  318. //# sourceMappingURL=experimental-hydrate.js.map