UNPKG

20.8 kBSource Map (JSON)View Raw
1{"version":3,"file":"experimental-hydrate.js","sources":["src/experimental-hydrate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\nimport type {TemplateResult} from './lit-html.js';\n\nimport {noChange, RenderOptions, _$LH} from './lit-html.js';\nimport {AttributePartInfo, PartType} from './directive.js';\nimport {\n isPrimitive,\n isSingleExpression,\n isTemplateResult,\n} from './directive-helpers.js';\n\nconst {\n _TemplateInstance: TemplateInstance,\n _isIterable: isIterable,\n _resolveDirective: resolveDirective,\n _ChildPart: ChildPart,\n _ElementPart: ElementPart,\n} = _$LH;\n\ntype ChildPart = InstanceType<typeof ChildPart>;\ntype TemplateInstance = InstanceType<typeof TemplateInstance>;\n\n/**\n * Information needed to rehydrate a single TemplateResult.\n */\ntype ChildPartState =\n | {\n type: 'leaf';\n /** The ChildPart that the result is rendered to */\n part: ChildPart;\n }\n | {\n type: 'iterable';\n /** The ChildPart that the result is rendered to */\n part: ChildPart;\n value: Iterable<unknown>;\n iterator: Iterator<unknown>;\n done: boolean;\n }\n | {\n type: 'template-instance';\n /** The ChildPart that the result is rendered to */\n part: ChildPart;\n\n result: TemplateResult;\n\n /** The TemplateInstance created from the TemplateResult */\n instance: TemplateInstance;\n\n /**\n * The index of the next Template part to be hydrated. This is mutable and\n * updated as the tree walk discovers new part markers at the right level in\n * the template instance tree. Note there is only one Template part per\n * attribute with (one or more) bindings.\n */\n templatePartIndex: number;\n\n /**\n * The index of the next TemplateInstance part to be hydrated. This is used\n * to retrieve the value from the TemplateResult and initialize the\n * TemplateInstance parts' values for dirty-checking on first render.\n */\n instancePartIndex: number;\n };\n\n/**\n * hydrate() operates on a container with server-side rendered content and\n * restores the client side data structures needed for lit-html updates such as\n * TemplateInstances and Parts. After calling `hydrate`, lit-html will behave as\n * if it initially rendered the DOM, and any subsequent updates will update\n * efficiently, the same as if lit-html had rendered the DOM on the client.\n *\n * hydrate() must be called on DOM that adheres the to lit-ssr structure for\n * parts. ChildParts must be represented with both a start and end comment\n * marker, and ChildParts that contain a TemplateInstance must have the template\n * digest written into the comment data.\n *\n * Since render() encloses its output in a ChildPart, there must always be a root\n * ChildPart.\n *\n * Example (using for # ... for annotations in HTML)\n *\n * Given this input:\n *\n * html`<div class=${x}>${y}</div>`\n *\n * The SSR DOM is:\n *\n * <!--lit-part AEmR7W+R0Ak=--> # Start marker for the root ChildPart created\n * # by render(). Includes the digest of the\n * # template\n * <div class=\"TEST_X\">\n * <!--lit-node 0--> # Indicates there are attribute bindings here\n * # The number is the depth-first index of the parent\n * # node in the template.\n * <!--lit-part--> # Start marker for the ${x} expression\n * TEST_Y\n * <!--/lit-part--> # End marker for the ${x} expression\n * </div>\n *\n * <!--/lit-part--> # End marker for the root ChildPart\n *\n * @param rootValue\n * @param container\n * @param userOptions\n */\nexport const hydrate = (\n rootValue: unknown,\n container: Element | DocumentFragment,\n options: Partial<RenderOptions> = {}\n) => {\n // TODO(kschaaf): Do we need a helper for _$litPart$ (\"part for node\")?\n // This property needs to remain unminified.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((container as any)['_$litPart$'] !== undefined) {\n throw new Error('container already contains a live render');\n }\n\n // Since render() creates a ChildPart to render into, we'll always have\n // exactly one root part. We need to hold a reference to it so we can set\n // it in the parts cache.\n let rootPart: ChildPart | undefined = undefined;\n\n // When we are in-between ChildPart markers, this is the current ChildPart.\n // It's needed to be able to set the ChildPart's endNode when we see a\n // close marker\n let currentChildPart: ChildPart | undefined = undefined;\n\n // Used to remember parent template state as we recurse into nested\n // templates\n const stack: Array<ChildPartState> = [];\n\n const walker = document.createTreeWalker(\n container,\n NodeFilter.SHOW_COMMENT,\n null,\n false\n );\n let marker: Comment | null;\n\n // Walk the DOM looking for part marker comments\n while ((marker = walker.nextNode() as Comment | null) !== null) {\n const markerText = marker.data;\n if (markerText.startsWith('lit-part')) {\n if (stack.length === 0 && rootPart !== undefined) {\n throw new Error('there must be only one root part per container');\n }\n // Create a new ChildPart and push it onto the stack\n currentChildPart = openChildPart(rootValue, marker, stack, options);\n rootPart ??= currentChildPart;\n } else if (markerText.startsWith('lit-node')) {\n // Create and hydrate attribute parts into the current ChildPart on the\n // stack\n createAttributeParts(marker, stack, options);\n // Remove `defer-hydration` attribute, if any\n const parent = marker.parentElement!;\n if (parent.hasAttribute('defer-hydration')) {\n parent.removeAttribute('defer-hydration');\n }\n } else if (markerText.startsWith('/lit-part')) {\n // Close the current ChildPart, and pop the previous one off the stack\n if (stack.length === 1 && currentChildPart !== rootPart) {\n throw new Error('internal error');\n }\n currentChildPart = closeChildPart(marker, currentChildPart, stack);\n }\n }\n console.assert(\n rootPart !== undefined,\n 'there should be exactly one root part in a render container'\n );\n // This property needs to remain unminified.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (container as any)['_$litPart$'] = rootPart;\n};\n\nconst openChildPart = (\n rootValue: unknown,\n marker: Comment,\n stack: Array<ChildPartState>,\n options: RenderOptions\n) => {\n let value: unknown;\n // We know the startNode now. We'll know the endNode when we get to\n // the matching marker and set it in closeChildPart()\n // TODO(kschaaf): Current constructor takes both nodes\n let part;\n if (stack.length === 0) {\n part = new ChildPart(marker, null, undefined, options);\n value = rootValue;\n } else {\n const state = stack[stack.length - 1];\n if (state.type === 'template-instance') {\n part = new ChildPart(marker, null, state.instance, options);\n state.instance._parts.push(part);\n value = state.result.values[state.instancePartIndex++];\n state.templatePartIndex++;\n } else if (state.type === 'iterable') {\n part = new ChildPart(marker, null, state.part, options);\n const result = state.iterator.next();\n if (result.done) {\n value = undefined;\n state.done = true;\n throw new Error('Unhandled shorter than expected iterable');\n } else {\n value = result.value;\n }\n (state.part._$committedValue as Array<ChildPart>).push(part);\n } else {\n // state.type === 'leaf'\n // TODO(kschaaf): This is unexpected, and likely a result of a primitive\n // been rendered on the client when a TemplateResult was rendered on the\n // server; this part will be hydrated but not used. We can detect it, but\n // we need to decide what to do in this case. Note that this part won't be\n // retained by any parent TemplateInstance, since a primitive had been\n // rendered in its place.\n // https://github.com/lit/lit/issues/1434\n // throw new Error('Hydration value mismatch: Found a TemplateInstance' +\n // 'where a leaf value was expected');\n part = new ChildPart(marker, null, state.part, options);\n }\n }\n\n // Initialize the ChildPart state depending on the type of value and push\n // it onto the stack. This logic closely follows the ChildPart commit()\n // cascade order:\n // 1. directive\n // 2. noChange\n // 3. primitive (note strings must be handled before iterables, since they\n // are iterable)\n // 4. TemplateResult\n // 5. Node (not yet implemented, but fallback handling is fine)\n // 6. Iterable\n // 7. nothing (handled in fallback)\n // 8. Fallback for everything else\n value = resolveDirective(part, value);\n if (value === noChange) {\n stack.push({part, type: 'leaf'});\n } else if (isPrimitive(value)) {\n stack.push({part, type: 'leaf'});\n part._$committedValue = value;\n // TODO(kschaaf): We can detect when a primitive is being hydrated on the\n // client where a TemplateResult was rendered on the server, but we need to\n // decide on a strategy for what to do next.\n // https://github.com/lit/lit/issues/1434\n // if (marker.data !== 'lit-part') {\n // throw new Error('Hydration value mismatch: Primitive found where TemplateResult expected');\n // }\n } else if (isTemplateResult(value)) {\n // Check for a template result digest\n const markerWithDigest = `lit-part ${digestForTemplateResult(value)}`;\n if (marker.data === markerWithDigest) {\n const template = ChildPart.prototype._$getTemplate(value);\n const instance = new TemplateInstance(template, part);\n stack.push({\n type: 'template-instance',\n instance,\n part,\n templatePartIndex: 0,\n instancePartIndex: 0,\n result: value,\n });\n // For TemplateResult values, we set the part value to the\n // generated TemplateInstance\n part._$committedValue = instance;\n } else {\n // TODO: if this isn't the server-rendered template, do we\n // need to stop hydrating this subtree? Clear it? Add tests.\n throw new Error(\n 'Hydration value mismatch: Unexpected TemplateResult rendered to part'\n );\n }\n } else if (isIterable(value)) {\n // currentChildPart.value will contain an array of ChildParts\n stack.push({\n part: part,\n type: 'iterable',\n value,\n iterator: value[Symbol.iterator](),\n done: false,\n });\n part._$committedValue = [];\n } else {\n // Fallback for everything else (nothing, Objects, Functions,\n // etc.): we just initialize the part's value\n // Note that `Node` value types are not currently supported during\n // SSR, so that part of the cascade is missing.\n stack.push({part: part, type: 'leaf'});\n part._$committedValue = value == null ? '' : value;\n }\n return part;\n};\n\nconst closeChildPart = (\n marker: Comment,\n part: ChildPart | undefined,\n stack: Array<ChildPartState>\n): ChildPart | undefined => {\n if (part === undefined) {\n throw new Error('unbalanced part marker');\n }\n\n part._$endNode = marker;\n\n const currentState = stack.pop()!;\n\n if (currentState.type === 'iterable') {\n if (!currentState.iterator.next().done) {\n throw new Error('unexpected longer than expected iterable');\n }\n }\n\n if (stack.length > 0) {\n const state = stack[stack.length - 1];\n return state.part;\n } else {\n return undefined;\n }\n};\n\nconst createAttributeParts = (\n comment: Comment,\n stack: Array<ChildPartState>,\n options: RenderOptions\n) => {\n // Get the nodeIndex from DOM. We're only using this for an integrity\n // check right now, we might not need it.\n const match = /lit-node (\\d+)/.exec(comment.data)!;\n const nodeIndex = parseInt(match[1]);\n\n // For void elements, the node the comment was referring to will be\n // the previousSibling; for non-void elements, the comment is guaranteed\n // to be the first child of the element (i.e. it won't have a previousSibling\n // meaning it should use the parentElement)\n const node = comment.previousSibling ?? comment.parentElement;\n\n const state = stack[stack.length - 1];\n if (state.type === 'template-instance') {\n const instance = state.instance;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n // If the next template part is in attribute-position on the current node,\n // create the instance part for it and prime its state\n const templatePart = instance._$template.parts[state.templatePartIndex];\n if (\n templatePart === undefined ||\n (templatePart.type !== PartType.ATTRIBUTE &&\n templatePart.type !== PartType.ELEMENT) ||\n templatePart.index !== nodeIndex\n ) {\n break;\n }\n\n if (templatePart.type === PartType.ATTRIBUTE) {\n // The instance part is created based on the constructor saved in the\n // template part\n const instancePart = new templatePart.ctor(\n node as HTMLElement,\n templatePart.name,\n templatePart.strings,\n state.instance,\n options\n );\n\n const value = isSingleExpression(\n instancePart as unknown as AttributePartInfo\n )\n ? state.result.values[state.instancePartIndex]\n : state.result.values;\n\n // Setting the attribute value primes committed value with the resolved\n // directive value; we only then commit that value for event/property\n // parts since those were not serialized, and pass `noCommit` for the\n // others to avoid perf impact of touching the DOM unnecessarily\n const noCommit = !(\n instancePart.type === PartType.EVENT ||\n instancePart.type === PartType.PROPERTY\n );\n instancePart._$setValue(\n value,\n instancePart,\n state.instancePartIndex,\n noCommit\n );\n state.instancePartIndex += templatePart.strings.length - 1;\n instance._parts.push(instancePart);\n } else {\n // templatePart.type === PartType.ELEMENT\n const instancePart = new ElementPart(\n node as HTMLElement,\n state.instance,\n options\n );\n resolveDirective(\n instancePart,\n state.result.values[state.instancePartIndex++]\n );\n instance._parts.push(instancePart);\n }\n state.templatePartIndex++;\n }\n } else {\n throw new Error('internal error');\n }\n};\n\n// Number of 32 bit elements to use to create template digests\nconst digestSize = 2;\n// We need to specify a digest to use across rendering environments. This is a\n// simple digest build from a DJB2-ish hash modified from:\n// https://github.com/darkskyapp/string-hash/blob/master/index.js\n// It has been changed to an array of hashes to add additional bits.\n// Goals:\n// - Extremely low collision rate. We may not be able to detect collisions.\n// - Extremely fast.\n// - Extremely small code size.\n// - Safe to include in HTML comment text or attribute value.\n// - Easily specifiable and implementable in multiple languages.\n// We don't care about cryptographic suitability.\nexport const digestForTemplateResult = (templateResult: TemplateResult) => {\n const hashes = new Uint32Array(digestSize).fill(5381);\n\n for (const s of templateResult.strings) {\n for (let i = 0; i < s.length; i++) {\n hashes[i % digestSize] = (hashes[i % digestSize] * 33) ^ s.charCodeAt(i);\n }\n }\n return btoa(String.fromCharCode(...new Uint8Array(hashes.buffer)));\n};\n"],"names":["_TemplateInstance","TemplateInstance","_isIterable","isIterable","_resolveDirective","resolveDirective","_ChildPart","ChildPart","_ElementPart","ElementPart","_$LH","hydrate","rootValue","container","options","undefined","Error","rootPart","currentChildPart","stack","walker","document","createTreeWalker","NodeFilter","SHOW_COMMENT","marker","nextNode","markerText","data","startsWith","length","openChildPart","createAttributeParts","parent","parentElement","hasAttribute","removeAttribute","closeChildPart","console","assert","value","part","state","type","instance","_parts","push","result","values","instancePartIndex","templatePartIndex","iterator","next","done","_$committedValue","noChange","isPrimitive","isTemplateResult","markerWithDigest","digestForTemplateResult","template","prototype","_$getTemplate","Symbol","_$endNode","currentState","pop","comment","match","exec","nodeIndex","parseInt","node","previousSibling","templatePart","_$template","parts","PartType","ATTRIBUTE","ELEMENT","index","instancePart","ctor","name","strings","isSingleExpression","noCommit","EVENT","PROPERTY","_$setValue","templateResult","hashes","Uint32Array","fill","s","i","charCodeAt","btoa","String","fromCharCode","Uint8Array","buffer"],"mappings":"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"}
\No newline at end of file