Skip to content

Revert "fix(ssr): fixes for scoped: true components during SSR (#6311)" #6336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 24 additions & 42 deletions src/runtime/client-hydrate.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';

/**
Expand Down Expand Up @@ -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') {
Expand All @@ -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);
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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']);
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -285,6 +267,7 @@ export const initializeClientHydrate = (
}
}

plt.$orgLocNodes$.delete(hostElm['s-id']);
hostRef.$hostElement$ = hostElm;
endHydrate();
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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$);

Expand Down Expand Up @@ -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 });
Expand Down
12 changes: 0 additions & 12 deletions src/runtime/connected-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/runtime/test/hydrate-shadow-in-shadow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,15 @@ describe('hydrate, shadow in shadow', () => {
<mock:shadow-root>
<slot></slot>
</mock:shadow-root>
<slot></slot>
</cmp-b>
</mock:shadow-root>
light-dom
<slot></slot>
</cmp-a>
`);
expect(clientHydrated.root).toEqualLightHtml(`
<cmp-a class="hydrated">
light-dom
<slot></slot>
</cmp-a>
`);
});
Expand Down
7 changes: 0 additions & 7 deletions src/runtime/update-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
9 changes: 4 additions & 5 deletions src/runtime/vdom/set-accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
114 changes: 0 additions & 114 deletions test/wdio/ssr-hydration/cmp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
<div>
<shadow-ssr-parent-cmp>
<div slot="things">one</div>
<div slot="things">2</div>
<div slot="things">3</div>
</shadow-ssr-parent-cmp>
</div>`,
{
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 <style> tag for `scoped-cmp` gets added
expect(wrapCmp.shadowRoot.querySelector('style[sty-id="sc-scoped-ssr-child-cmp"]')).toBeTruthy();
});

it('slots nodes appropriately in a `scoped: true` parent with `serializeShadowRoot: "scoped"` child', async () => {
if (document.querySelector('#stage')) {
document.querySelector('#stage')?.remove();
await browser.waitUntil(async () => !document.querySelector('#stage'));
}
const { html } = await renderToString(
`
<div>
<scoped-ssr-parent-cmp>
<div slot="things">one</div>
<div slot="things">2</div>
<div slot="things">3</div>
</scoped-ssr-parent-cmp>
</div>`,
{
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('scoped-ssr-parent-cmp'));
expect(typeof customElements.get('scoped-ssr-parent-cmp')).toBe('function');

const wrapCmp = document.querySelector('scoped-ssr-parent-cmp');
expect(wrapCmp.childNodes.length).toBe(3);
expect(wrapCmp.textContent).toBe('one23');
});

it('correctly renders a slow to hydrate component with a prop', async () => {
if (document.querySelector('#stage')) {
document.querySelector('#stage')?.remove();
await browser.waitUntil(async () => !document.querySelector('#stage'));
}
const { html } = await renderToString(`<slow-ssr-prop></slow-ssr-prop>`, {
fullDocument: true,
serializeShadowRoot: 'declarative-shadow-dom',
beforeHydrate: (doc) => {
// simulate a slow prop update
const slowCmp = doc.querySelector('slow-ssr-prop');
slowCmp.anArray = ['one', 'two', 'three'];
},
});
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('slow-ssr-prop'));
expect(typeof customElements.get('slow-ssr-prop')).toBe('function');

const slowCmp: any = document.querySelector('slow-ssr-prop');
setTimeout(() => {
slowCmp.anArray = ['one', 'two', 'three', 'four'];
}, 400);
await browser.pause(600);

expect(slowCmp.shadowRoot.querySelector('div').textContent).toBe('An array component:onetwothreefour');
});
});
23 changes: 0 additions & 23 deletions test/wdio/ssr-hydration/scoped-child-cmp.tsx

This file was deleted.

Loading
Loading