diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index c07ac998d35..fa18ef3487d 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -1,10 +1,10 @@ import { BUILD } from '@app-data'; -import { getHostRef, plt, win } from '@platform'; +import { plt, win } from '@platform'; import { parsePropertyValue } from '@runtime'; import { CMP_FLAGS, MEMBER_FLAGS } from '@utils'; import type * as d from '../declarations'; -import { internalCall, patchSlottedNode } from './dom-extras'; +import { patchSlottedNode } from './dom-extras'; import { createTime } from './profile'; import { COMMENT_NODE_ID, @@ -18,7 +18,6 @@ import { VNODE_FLAGS, } from './runtime-constants'; import { addSlotRelocateNode, patchSlotNode } from './slot-polyfill-utils'; -import { getScopeId } from './styles'; import { newVNode } from './vdom/h'; /** @@ -130,19 +129,6 @@ export const initializeClientHydrate = ( // If we don't, `vdom-render.ts` will try to add nodes to it (and because it may be a comment node, it will error) node['s-cr'] = hostElm['s-cr']; } - } else if (childRenderNode.$tag$?.toString().includes('-') && !childRenderNode.$elm$.shadowRoot) { - // if this child is a non-shadow component being added to a shadowDOM, - // let's find and add its styles to the shadowRoot, so we don't get a visual flicker - const cmpMeta = getHostRef(childRenderNode.$elm$); - const scopeId = getScopeId( - cmpMeta.$cmpMeta$, - BUILD.mode ? childRenderNode.$elm$.getAttribute('s-mode') : undefined, - ); - const styleSheet = win.document.querySelector(`style[sty-id="${scopeId}"]`); - - if (styleSheet) { - hostElm.shadowRoot.append(styleSheet.cloneNode(true)); - } } if (childRenderNode.$tag$ === 'slot') { @@ -164,7 +150,7 @@ export const initializeClientHydrate = ( } if (orgLocationNode && orgLocationNode.isConnected) { - if (orgLocationNode.parentElement.shadowRoot && orgLocationNode['s-en'] === '') { + if (shadowRoot && orgLocationNode['s-en'] === '') { // if this node is within a shadowDOM, with an original location home // we're safe to move it now orgLocationNode.parentNode.insertBefore(node, orgLocationNode.nextSibling); @@ -180,9 +166,7 @@ export const initializeClientHydrate = ( } } // Remove the original location from the map - if (orgLocationNode && !orgLocationNode['s-id']) { - plt.$orgLocNodes$.delete(orgLocationId); - } + plt.$orgLocNodes$.delete(orgLocationId); } const hosts: d.HostElement[] = []; @@ -219,18 +203,16 @@ export const initializeClientHydrate = ( if (!hostEle.shadowRoot || !shadowRoot) { // Try to set an appropriate Content-position Reference (CR) node for this host element - if (!slottedItem.slot['s-cr']) { - // Is a CR already set on the host? - slottedItem.slot['s-cr'] = hostEle['s-cr']; - - if (!slottedItem.slot['s-cr'] && hostEle.shadowRoot) { - // Host has shadowDOM - just use the host itself as the CR for native slotting - slottedItem.slot['s-cr'] = hostEle; - } else { - // If all else fails - just set the CR as the first child - // (9/10 if node['s-cr'] hasn't been set, the node will be at the element root) - slottedItem.slot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; - } + // Is a CR already set on the host? + slottedItem.slot['s-cr'] = hostEle['s-cr']; + + if (!slottedItem.slot['s-cr'] && hostEle.shadowRoot) { + // Host has shadowDOM - just use the host itself as the CR for native slotting + slottedItem.slot['s-cr'] = hostEle; + } else { + // If all else fails - just set the CR as the first child + // (9/10 if node['s-cr'] hasn't been set, the node will be at the element root) + slottedItem.slot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; } // Create our 'Original Location' node addSlotRelocateNode(slottedItem.node, slottedItem.slot, false, slottedItem.node['s-oo']); @@ -255,7 +237,7 @@ export const initializeClientHydrate = ( }); } - if (BUILD.shadowDom && shadowRoot) { + if (BUILD.shadowDom && shadowRoot && !shadowRoot.childNodes.length) { // For `scoped` shadowDOM rendering (not DSD); // Add all the root nodes in the shadowDOM (a root node can have a whole nested DOM tree) let rnIdex = 0; @@ -273,7 +255,7 @@ export const initializeClientHydrate = ( // we can safely leave it be, native behavior will mean it's hidden (node as HTMLElement).removeAttribute('hidden'); } else if ( - (node.nodeType === NODE_TYPE.CommentNode && !node.nodeValue) || + node.nodeType === NODE_TYPE.CommentNode || (node.nodeType === NODE_TYPE.TextNode && !(node as Text).wholeText.trim()) ) { // During `scoped` shadowDOM rendering, there's a bunch of comment nodes used for positioning / empty text nodes. @@ -285,6 +267,7 @@ export const initializeClientHydrate = ( } } + plt.$orgLocNodes$.delete(hostElm['s-id']); hostRef.$hostElement$ = hostElm; endHydrate(); }; @@ -353,7 +336,7 @@ const clientHydrate = ( parentVNode.$children$ = []; } - if (BUILD.scoped && scopeId && childIdSplt[0] === hostId) { + if (BUILD.scoped && scopeId) { // Host is `scoped: true` - add that flag to the child. // It's used in 'set-accessor.ts' to make sure our scoped class is present node['s-si'] = scopeId; @@ -526,7 +509,7 @@ const clientHydrate = ( vnode.$index$ = '0'; parentVNode.$children$ = [vnode]; } else { - if (node.nodeType === NODE_TYPE.TextNode && !(node as unknown as Text).wholeText.trim() && !node['s-nr']) { + if (node.nodeType === NODE_TYPE.TextNode && !(node as unknown as Text).wholeText.trim()) { // empty white space is never accounted for from SSR so there's // no corresponding comment node giving it a position in the DOM. // It therefore gets slotted / clumped together at the end of the host. @@ -630,13 +613,13 @@ function addSlot( childVNode.$elm$.setAttribute('name', slotName); } - if (parentVNode.$elm$.shadowRoot && parentNodeId && parentNodeId !== childVNode.$hostId$) { + if (parentNodeId && parentNodeId !== childVNode.$hostId$) { // Shadow component's slot is placed inside a nested component's shadowDOM; it doesn't belong to this host - it was forwarded by the SSR markup. // Insert it in the root of this host; it's lightDOM. It doesn't really matter where in the host root; the component will take care of it. - internalCall(parentVNode.$elm$, 'insertBefore')(slot, internalCall(parentVNode.$elm$, 'children')[0]); + parentVNode.$elm$.insertBefore(slot, parentVNode.$elm$.children[0]); } else { // Insert the new slot element before the slot comment - internalCall(internalCall(node, 'parentNode') as d.RenderNode, 'insertBefore')(slot, node); + node.parentNode.insertBefore(slot, node); } addSlottedNodes(slottedNodes, slotId, slotName, node, childVNode.$hostId$); @@ -701,9 +684,8 @@ const addSlottedNodes = ( (((slottedNode['getAttribute'] && slottedNode.getAttribute('slot')) || slottedNode['s-sn']) === slotName || (slotName === '' && !slottedNode['s-sn'] && - (slottedNode.nodeType === NODE_TYPE.CommentNode || - slottedNode.nodeType === NODE_TYPE.TextNode || - slottedNode.tagName === 'SLOT'))) + ((slottedNode.nodeType === NODE_TYPE.CommentNode && slottedNode.nodeValue.indexOf('.') !== 1) || + slottedNode.nodeType === NODE_TYPE.TextNode))) ) { slottedNode['s-sn'] = slotName; slottedNodes[slotNodeId as any].push({ slot: slotNode, node: slottedNode, hostId }); diff --git a/src/runtime/connected-callback.ts b/src/runtime/connected-callback.ts index f2d859db9cd..209aa1fb3cb 100644 --- a/src/runtime/connected-callback.ts +++ b/src/runtime/connected-callback.ts @@ -57,18 +57,6 @@ export const connectedCallback = (elm: d.HostElement) => { cmpMeta.$flags$ & (CMP_FLAGS.hasSlotRelocation | CMP_FLAGS.needsShadowDomShim)) ) { setContentReference(elm); - } else if (BUILD.hydrateClientSide && !(cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation)) { - const commendPlaceholder = elm.firstChild as d.RenderNode; - if ( - commendPlaceholder?.nodeType === NODE_TYPE.CommentNode && - !commendPlaceholder['s-cn'] && - !commendPlaceholder.nodeValue - ) { - // if the first child is a comment node that was created by the - // setContentReference() function during SSR, remove it now as - // this component does not need slot relocation and can cause hydration issues - elm.removeChild(commendPlaceholder); - } } } diff --git a/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx b/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx index 86a43d355f6..1ff3c9c3576 100644 --- a/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx +++ b/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx @@ -60,16 +60,15 @@ describe('hydrate, shadow in shadow', () => { + light-dom - `); expect(clientHydrated.root).toEqualLightHtml(` light-dom - `); }); diff --git a/src/runtime/update-component.ts b/src/runtime/update-component.ts index 3eef3f48559..bf9da7b694e 100644 --- a/src/runtime/update-component.ts +++ b/src/runtime/update-component.ts @@ -407,13 +407,6 @@ export const appDidLoad = (who: string) => { plt.$flags$ |= PLATFORM_FLAGS.appLoaded; } nextTick(() => emitEvent(win, 'appload', { detail: { namespace: NAMESPACE } })); - if (BUILD.hydrateClientSide) { - // we can now clear out the original location map - // used by SSR so as to not cause memory leaks - if (plt.$orgLocNodes$?.size) { - plt.$orgLocNodes$.clear(); - } - } if (BUILD.profile && performance.measure) { performance.measure(`[Stencil] ${NAMESPACE} initial load (by ${who})`, 'st:app:start'); diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts index 5f25743a6ff..074afa815e2 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/src/runtime/vdom/set-accessor.ts @@ -50,15 +50,14 @@ export const setAccessor = ( const oldClasses = parseClassList(oldValue); let newClasses = parseClassList(newValue); - if (BUILD.hydrateClientSide && (elm['s-si'] || elm['s-sc']) && initialRender) { + if (BUILD.hydrateClientSide && elm['s-si'] && initialRender) { // for `scoped: true` components, new nodes after initial hydration // from SSR don't have the slotted class added. Let's add that now - const scopeId = elm['s-sc'] || elm['s-si']; - newClasses.push(scopeId); + newClasses.push(elm['s-si']); oldClasses.forEach((c) => { - if (c.startsWith(scopeId)) newClasses.push(c); + if (c.startsWith(elm['s-si'])) newClasses.push(c); }); - newClasses = [...new Set(newClasses)].filter((c) => c); + newClasses = [...new Set(newClasses)]; classList.add(...newClasses); } else { classList.remove(...oldClasses.filter((c) => c && !newClasses.includes(c))); diff --git a/test/wdio/ssr-hydration/cmp.test.tsx b/test/wdio/ssr-hydration/cmp.test.tsx index 4eeae8710c0..bec30e990e8 100644 --- a/test/wdio/ssr-hydration/cmp.test.tsx +++ b/test/wdio/ssr-hydration/cmp.test.tsx @@ -423,118 +423,4 @@ describe('Sanity check SSR > Client hydration', () => { expect((nestedCmp.childNodes[0] as HTMLElement).tagName).toBe('SLOT'); expect(nestedCmp.childNodes[1].textContent).toBe('after'); }); - - it('renders slot nodes appropriately in a `scoped: true` child with `serializeShadowRoot: "scoped"` parent', async () => { - if (document.querySelector('#stage')) { - document.querySelector('#stage')?.remove(); - await browser.waitUntil(async () => !document.querySelector('#stage')); - } - const { html } = await renderToString( - ` -
- -
one
-
2
-
3
-
-
`, - { - fullDocument: true, - serializeShadowRoot: 'scoped', - }, - ); - const stage = document.createElement('div'); - stage.setAttribute('id', 'stage'); - stage.setHTMLUnsafe(html); - document.body.appendChild(stage); - - // @ts-expect-error resolved through WDIO - const { defineCustomElements } = await import('/dist/loader/index.js'); - defineCustomElements().catch(console.error); - - // wait for Stencil to take over and reconcile - await browser.waitUntil(async () => customElements.get('shadow-ssr-parent-cmp')); - expect(typeof customElements.get('shadow-ssr-parent-cmp')).toBe('function'); - - const wrapCmp = document.querySelector('shadow-ssr-parent-cmp'); - const nestedCmp = wrapCmp.shadowRoot.querySelector('scoped-ssr-child-cmp'); - expect(nestedCmp.childNodes.length).toBe(1); - expect((nestedCmp.childNodes[0] as HTMLElement).tagName).toBe('SLOT'); - - // check that