diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 5dbe86c078..cfa6974298 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -41,6 +41,8 @@ import { VComment, VElementData, VNodeType, + VStatic, + Key, } from './vnodes'; const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; @@ -49,6 +51,18 @@ function addVNodeToChildLWC(vnode: VCustomElement) { ArrayPush.call(getVMBeingRendered()!.velements, vnode); } +// [st]atic node +function st(fragment: Element, key: Key): VStatic { + return { + type: VNodeType.Static, + sel: undefined, + key, + elm: undefined, + fragment, + owner: getVMBeingRendered()!, + }; +} + // [h]tml node function h(sel: string, data: VElementData, children: VNodes = EmptyArray): VElement { const vmBeingRendered = getVMBeingRendered()!; @@ -546,6 +560,7 @@ const api = ObjectFreeze({ co, dc, ti, + st, gid, fid, shc, diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts index 627a34ae2f..2109322568 100644 --- a/packages/@lwc/engine-core/src/framework/hydration.ts +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -29,6 +29,7 @@ import { VComment, VElement, VCustomElement, + VStatic, } from './vnodes'; import { patchProps } from './modules/props'; @@ -81,6 +82,11 @@ function hydrateNode(node: Node, vnode: VNode, renderer: RendererAPI): Node | nu hydratedNode = hydrateComment(node, vnode, renderer); break; + case VNodeType.Static: + // VStatic are cacheable and cannot have custom renderer associated to them + hydratedNode = hydrateStaticElement(node, vnode, renderer); + break; + case VNodeType.Element: hydratedNode = hydrateElement(node, vnode, vnode.data.renderer ?? renderer); break; @@ -137,6 +143,16 @@ function hydrateComment(node: Node, vnode: VComment, renderer: RendererAPI): Nod return node; } +function hydrateStaticElement(elm: Node, vnode: VStatic, renderer: RendererAPI): Node | null { + if (!areCompatibleNodes(vnode.fragment, elm, vnode, renderer)) { + return handleMismatch(elm, vnode, renderer); + } + + vnode.elm = elm; + + return elm; +} + function hydrateElement(elm: Node, vnode: VElement, renderer: RendererAPI): Node | null { if ( !hasCorrectNodeType(vnode, elm, EnvNodeTypes.ELEMENT, renderer) || @@ -481,3 +497,61 @@ function validateStyleAttr(vnode: VBaseElement, elm: Element, renderer: Renderer return nodesAreCompatible; } + +function areCompatibleNodes(client: Node, ssr: Node, vnode: VNode, renderer: RendererAPI) { + const { getProperty, getAttribute } = renderer; + if (getProperty(client, 'nodeType') === EnvNodeTypes.TEXT) { + if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.TEXT, renderer)) { + return false; + } + + return getProperty(client, 'nodeValue') === getProperty(ssr, 'nodeValue'); + } + + if (getProperty(client, 'nodeType') === EnvNodeTypes.COMMENT) { + if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.COMMENT, renderer)) { + return false; + } + + return getProperty(client, 'nodeValue') === getProperty(ssr, 'nodeValue'); + } + + if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.ELEMENT, renderer)) { + return false; + } + + let isCompatibleElements = true; + if (getProperty(client, 'tagName') !== getProperty(ssr, 'tagName')) { + if (process.env.NODE_ENV !== 'production') { + logError( + `Hydration mismatch: expecting element with tag "${getProperty( + client, + 'tagName' + ).toLowerCase()}" but found "${getProperty(ssr, 'tagName').toLowerCase()}".`, + vnode.owner + ); + } + + return false; + } + + const clientAttrsNames: string[] = getProperty(client, 'getAttributeNames').call(client); + + clientAttrsNames.forEach((attrName) => { + if (getAttribute(client, attrName) !== getAttribute(ssr, attrName)) { + logError( + `Mismatch hydrating element <${getProperty( + client, + 'tagName' + ).toLowerCase()}>: attribute "${attrName}" has different values, expected "${getAttribute( + client, + attrName + )}" but found "${getAttribute(ssr, attrName)}"`, + vnode.owner + ); + isCompatibleElements = false; + } + }); + + return isCompatibleElements; +} diff --git a/packages/@lwc/engine-core/src/framework/main.ts b/packages/@lwc/engine-core/src/framework/main.ts index b7f87ea1c1..07c9e0ec3b 100644 --- a/packages/@lwc/engine-core/src/framework/main.ts +++ b/packages/@lwc/engine-core/src/framework/main.ts @@ -26,6 +26,7 @@ export { getAssociatedVMIfPresent, } from './vm'; +export { parseFragment, parseSVGFragment } from './template'; export { hydrateRoot } from './hydration'; // Internal APIs used by compiled code ------------------------------------------------------------- diff --git a/packages/@lwc/engine-core/src/framework/renderer.ts b/packages/@lwc/engine-core/src/framework/renderer.ts index 65940508aa..7dddb2e7f5 100644 --- a/packages/@lwc/engine-core/src/framework/renderer.ts +++ b/packages/@lwc/engine-core/src/framework/renderer.ts @@ -18,6 +18,8 @@ export interface RendererAPI { isHydrating: () => boolean; insert: (node: N, parent: E, anchor: N | null) => void; remove: (node: N, parent: E) => void; + cloneNode: (node: N, deep: boolean) => N; + createFragment: (html: string) => N | null; createElement: (tagName: string, namespace?: string) => E; createText: (content: string) => N; createComment: (content: string) => N; diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index ed6da6915e..e3813fd0b4 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -16,6 +16,7 @@ import { keys, SVG_NAMESPACE, KEY__SHADOW_RESOLVER, + KEY__SHADOW_STATIC, } from '@lwc/shared'; import { RendererAPI } from './renderer'; @@ -48,6 +49,7 @@ import { isVBaseElement, isSameVnode, VNodeType, + VStatic, } from './vnodes'; import { patchAttributes } from './modules/attrs'; @@ -98,6 +100,10 @@ function patch(n1: VNode, n2: VNode, renderer: RendererAPI) { patchComment(n1 as VComment, n2, renderer); break; + case VNodeType.Static: + n2.elm = n1.elm; + break; + case VNodeType.Element: patchElement(n1 as VElement, n2, n2.data.renderer ?? renderer); break; @@ -120,6 +126,11 @@ export function mount(node: VNode, parent: ParentNode, renderer: RendererAPI, an mountComment(node, parent, anchor, renderer); break; + case VNodeType.Static: + // VStatic cannot have a custom renderer associated to them, using owner's renderer + mountStatic(node, parent, anchor, renderer); + break; + case VNodeType.Element: // If the vnode data has a renderer override use it, else fallback to owner's renderer mountElement(node, parent, anchor, node.data.renderer ?? renderer); @@ -208,6 +219,35 @@ function patchElement(n1: VElement, n2: VElement, renderer: RendererAPI) { patchChildren(n1.children, n2.children, elm, renderer); } +function mountStatic( + vnode: VStatic, + parent: ParentNode, + anchor: Node | null, + renderer: RendererAPI +) { + const { owner } = vnode; + const { cloneNode, isSyntheticShadowDefined, insertNode } = renderer; + const elm = (vnode.elm = cloneNode(vnode.fragment, true)); + + linkNodeToShadow(elm, owner, renderer); + + // Marks this node as Static to propagate the shadow resolver. must happen after elm is assigned to the proper shadow + const { renderMode, shadowMode } = owner; + + if (isSyntheticShadowDefined) { + if (shadowMode === ShadowMode.Synthetic || renderMode === RenderMode.Light) { + (elm as any)[KEY__SHADOW_STATIC] = true; + } + } + + if (process.env.NODE_ENV !== 'production') { + const isLight = renderMode === RenderMode.Light; + patchElementWithRestrictions(elm, { isPortal: false, isLight }); + } + + insertNode(elm, parent, anchor); +} + function mountCustomElement( vnode: VCustomElement, parent: ParentNode, diff --git a/packages/@lwc/engine-core/src/framework/template.ts b/packages/@lwc/engine-core/src/framework/template.ts index f0db9d77dc..02084027ff 100644 --- a/packages/@lwc/engine-core/src/framework/template.ts +++ b/packages/@lwc/engine-core/src/framework/template.ts @@ -14,31 +14,32 @@ import { isNull, isTrue, isUndefined, - toString, KEY__SCOPED_CSS, + toString, } from '@lwc/shared'; import { logError } from '../shared/logger'; import { getComponentTag } from '../shared/format'; - +import { createFragment, getFirstChild } from '../renderer'; import api, { RenderAPI } from './api'; import { + RenderMode, resetComponentRoot, runWithBoundaryProtection, + ShadowMode, SlotSet, TemplateCache, VM, - RenderMode, } from './vm'; import { EmptyArray } from './utils'; import { defaultEmptyTemplate, isTemplateRegistered } from './secure-template'; import { - TemplateStylesheetFactories, createStylesheet, getStylesheetsContent, + TemplateStylesheetFactories, updateStylesheetToken, } from './stylesheet'; -import { logOperationStart, logOperationEnd, OperationId } from './profiler'; +import { logOperationEnd, logOperationStart, OperationId } from './profiler'; import { getTemplateOrSwappedTemplate, setActiveVM } from './hot-swaps'; import { VNodes } from './vnodes'; @@ -113,6 +114,77 @@ function validateLightDomTemplate(template: Template, vm: VM) { } } +const enum FragmentCache { + HAS_SCOPED_STYLE = 1 << 0, + SHADOW_MODE_SYNTHETIC = 1 << 1, +} + +function buildParseFragmentFn( + createFragmentFn: (html: string) => Element +): (strings: string[], ...keys: number[]) => () => Element { + return (strings: string[], ...keys: number[]) => { + const cache = create(null); + + return function (): Element { + const { + context: { hasScopedStyles, stylesheetToken }, + shadowMode, + } = getVMBeingRendered()!; + const hasStyleToken = !isUndefined(stylesheetToken); + const isSyntheticShadow = shadowMode === ShadowMode.Synthetic; + + let cacheKey = 0; + if (hasStyleToken && hasScopedStyles) { + cacheKey |= FragmentCache.HAS_SCOPED_STYLE; + } + if (hasStyleToken && isSyntheticShadow) { + cacheKey |= FragmentCache.SHADOW_MODE_SYNTHETIC; + } + + if (!isUndefined(cache[cacheKey])) { + return cache[cacheKey]; + } + + const classToken = hasScopedStyles && hasStyleToken ? ' ' + stylesheetToken : ''; + const classAttrToken = + hasScopedStyles && hasStyleToken ? ` class="${stylesheetToken}"` : ''; + const attrToken = hasStyleToken && isSyntheticShadow ? ' ' + stylesheetToken : ''; + + let htmlFragment = ''; + for (let i = 0, n = keys.length; i < n; i++) { + switch (keys[i]) { + case 0: // styleToken in existing class attr + htmlFragment += strings[i] + classToken; + break; + case 1: // styleToken for added class attr + htmlFragment += strings[i] + classAttrToken; + break; + case 2: // styleToken as attr + htmlFragment += strings[i] + attrToken; + break; + case 3: // ${1}${2} + htmlFragment += strings[i] + classAttrToken + attrToken; + break; + } + } + + htmlFragment += strings[strings.length - 1]; + + cache[cacheKey] = createFragmentFn(htmlFragment); + + return cache[cacheKey]; + }; + }; +} + +// Note: at the moment this code executes, createFragment have not being set. +export const parseFragment = buildParseFragmentFn((html) => createFragment(html)); +export const parseSVGFragment = buildParseFragmentFn((html) => { + const fragment = createFragment('' + html + ''); + + return getFirstChild(fragment); +}); + export function evaluateTemplate(vm: VM, html: Template): VNodes { if (process.env.NODE_ENV !== 'production') { assert.isTrue( diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index ee9f43f09a..d8cc25db89 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -752,4 +752,4 @@ export function forceRehydration(vm: VM) { markComponentAsDirty(vm); scheduleRehydration(vm); } -} +} \ No newline at end of file diff --git a/packages/@lwc/engine-core/src/framework/vnodes.ts b/packages/@lwc/engine-core/src/framework/vnodes.ts index e93fc611f7..e3fc2dc9dc 100644 --- a/packages/@lwc/engine-core/src/framework/vnodes.ts +++ b/packages/@lwc/engine-core/src/framework/vnodes.ts @@ -15,9 +15,10 @@ export const enum VNodeType { Comment, Element, CustomElement, + Static, } -export type VNode = VText | VComment | VElement | VCustomElement; +export type VNode = VText | VComment | VElement | VCustomElement | VStatic; export type VParentElement = VElement | VCustomElement; export type VNodes = Readonly>; @@ -29,6 +30,13 @@ export interface BaseVNode { owner: VM; } +export interface VStatic extends BaseVNode { + type: VNodeType.Static; + sel: undefined; + key: Key; + fragment: Element; +} + export interface VText extends BaseVNode { type: VNodeType.Text; sel: undefined; diff --git a/packages/@lwc/engine-dom/src/index.ts b/packages/@lwc/engine-dom/src/index.ts index fec06f876c..510c5a6064 100644 --- a/packages/@lwc/engine-dom/src/index.ts +++ b/packages/@lwc/engine-dom/src/index.ts @@ -11,6 +11,9 @@ import './polyfills/aria-properties/main'; // Tests ------------------------------------------------------------------------------------------- import './testFeatureFlag.ts'; +// Tests ------------------------------------------------------------------------------------------- +import './testFeatureFlag.ts'; + // Engine-core public APIs ------------------------------------------------------------------------- export { createContextProvider, @@ -30,6 +33,8 @@ export { setHooks, getComponentDef, isComponentConstructor, + parseFragment, + parseSVGFragment, swapComponent, swapStyle, swapTemplate, diff --git a/packages/@lwc/engine-dom/src/renderer.ts b/packages/@lwc/engine-dom/src/renderer.ts index 313d642e1d..e2d6f5c052 100644 --- a/packages/@lwc/engine-dom/src/renderer.ts +++ b/packages/@lwc/engine-dom/src/renderer.ts @@ -100,6 +100,14 @@ export const isSyntheticShadowDefined: boolean = hasOwnProperty.call( KEY__SHADOW_TOKEN ); +function cloneNode(node: Node, deep: boolean): Node { + return node.cloneNode(deep); +} + +function createFragment(html: string): Node | null { + return document.createRange().createContextualFragment(html).firstChild; +} + function createElement(tagName: string, namespace?: string): Element { return isUndefined(namespace) ? document.createElement(tagName) @@ -286,6 +294,8 @@ export const renderer = { isHydrating, insert, remove, + cloneNode, + createFragment, createElement, createText, createComment, diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-static/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-static/expected.html index 04a6a3b5e2..f80834225d 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-static/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-static/expected.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html index ab7774509d..55f49c209f 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-style/expected.html @@ -1,12 +1,12 @@