Skip to content
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

poc: ssr client rehydration #2442

Merged
merged 12 commits into from
Oct 26, 2021
Merged
9 changes: 9 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ commands:
type: boolean
compat:
type: boolean
test_hydrate:
type: boolean
default: false
coverage:
type: boolean
default: true
Expand All @@ -116,6 +119,7 @@ commands:
<<# parameters.disable_synthetic >> DISABLE_SYNTHETIC=1 <</ parameters.disable_synthetic >>
<<# parameters.force_native_shadow_mode >> FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 <</ parameters.force_native_shadow_mode >>
<<# parameters.compat >> COMPAT=1 <</ parameters.compat >>
<<# parameters.test_hydrate >> TEST_HYDRATION=1 <</ parameters.test_hydrate >>
<<# parameters.coverage >> COVERAGE=1 <</ parameters.coverage >>
yarn sauce

Expand Down Expand Up @@ -190,6 +194,11 @@ jobs:
disable_synthetic: false
compat: false
force_native_shadow_mode: true
- run_karma:
disable_synthetic: true
compat: false
force_native_shadow_mode: false
test_hydrate: true
- run:
name: Compute karma coverage
command: yarn coverage
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/babel-plugin-component/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const LWC_SUPPORTED_APIS = new Set([
'unwrap',

// From "@lwc/engine-dom"
'hydrateComponent',
'buildCustomElementConstructor',
'createElement',

Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/3rdparty/snabbdom/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface Hooks<N extends VNode> {
move: (vNode: N, parentNode: Node, referenceNode: Node | null) => void;
update: (oldVNode: N, vNode: N) => void;
remove: (vNode: N, parentNode: Node) => void;
hydrate: (vNode: N, node: Node) => void;
}

export interface Module<N extends VNode> {
Expand Down
118 changes: 115 additions & 3 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import {
StringReplace,
toString,
} from '@lwc/shared';
import { logError } from '../shared/logger';
import { RenderMode } from './vm';
import { logError, logWarn } from '../shared/logger';
import { invokeEventListener } from './invoker';
import { getVMBeingRendered } from './template';
import { EmptyArray, EmptyObject } from './utils';
import {
appendVM,
getAssociatedVMIfPresent,
getAssociatedVM,
removeVM,
rerenderVM,
runConnectedCallback,
Expand All @@ -39,6 +39,9 @@ import {
VM,
VMState,
getRenderRoot,
createVM,
hydrateVM,
RenderMode,
} from './vm';
import {
VNode,
Expand Down Expand Up @@ -66,8 +69,11 @@ import {
updateChildrenHook,
allocateChildrenHook,
markAsDynamicChildren,
hydrateChildrenHook,
hydrateElmHook,
LWCDOMMode,
} from './hooks';
import { isComponentConstructor } from './def';
import { getComponentInternalDef, isComponentConstructor } from './def';
import { getUpgradableConstructor } from './upgradable-element';

const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
Expand All @@ -86,6 +92,26 @@ const TextHook: Hooks<VText> = {
insert: insertNodeHook,
move: insertNodeHook, // same as insert for text nodes
remove: removeNodeHook,
hydrate: (vNode: VNode, node: Node) => {
jodarove marked this conversation as resolved.
Show resolved Hide resolved
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line lwc-internal/no-global-node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to confirm my understanding: it is safe to disable this rule because hydrate will never be invoked during unload event, as described in #2472. Is this correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, is safe because hydration only will be for engine-dom (so Node will be defined), and this another question/todo I have, is that IMO the hydrate implementation should live in the @lwc/engine-dom (instead of engine-core, where is today). but it can be done after this pr is merged.

if (node.nodeType !== Node.TEXT_NODE) {
logError('Hydration mismatch: incorrect node type received', vNode.owner);
assert.fail('Hydration mismatch: incorrect node type received.');
}

if (node.nodeValue !== vNode.text) {
logWarn(
'Hydration mismatch: text values do not match, will recover from the difference',
vNode.owner
);
}
}

// always set the text value to the one from the vnode.
node.nodeValue = vNode.text ?? null;
vNode.elm = node;
},
};

const CommentHook: Hooks<VComment> = {
Expand All @@ -101,6 +127,26 @@ const CommentHook: Hooks<VComment> = {
insert: insertNodeHook,
move: insertNodeHook, // same as insert for text nodes
remove: removeNodeHook,
hydrate: (vNode: VNode, node: Node) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line lwc-internal/no-global-node
if (node.nodeType !== Node.COMMENT_NODE) {
logError('Hydration mismatch: incorrect node type received', vNode.owner);
assert.fail('Hydration mismatch: incorrect node type received.');
}

if (node.nodeValue !== vNode.text) {
logWarn(
'Hydration mismatch: comment values do not match, will recover from the difference',
vNode.owner
);
}
}

// always set the text value to the one from the vnode.
node.nodeValue = vNode.text ?? null;
vNode.elm = node;
},
};

// insert is called after update, which is used somewhere else (via a module)
Expand Down Expand Up @@ -141,6 +187,39 @@ const ElementHook: Hooks<VElement> = {
removeNodeHook(vnode, parentNode);
removeElmHook(vnode);
},
hydrate: (vnode, node) => {
const elm = node as Element;
vnode.elm = elm;

const { context } = vnode.data;
const isDomManual = Boolean(
!isUndefined(context) &&
!isUndefined(context.lwc) &&
context.lwc.dom === LWCDOMMode.manual
);

if (isDomManual) {
// it may be that this element has lwc:inner-html, we need to diff and in case are the same,
// remove the innerHTML from props so it reuses the existing dom elements.
const { props } = vnode.data;
if (!isUndefined(props) && !isUndefined(props.innerHTML)) {
if (elm.innerHTML === props.innerHTML) {
delete props.innerHTML;
} else {
logWarn(
`Mismatch hydrating element <${elm.tagName.toLowerCase()}>: innerHTML values do not match for element, will recover from the difference`,
vnode.owner
);
}
}
}

hydrateElmHook(vnode);

if (!isDomManual) {
hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vnode.owner);
}
},
};

const CustomElementHook: Hooks<VCustomElement> = {
Expand Down Expand Up @@ -219,6 +298,39 @@ const CustomElementHook: Hooks<VCustomElement> = {
removeVM(vm);
}
},
hydrate: (vnode, elm) => {
// the element is created, but the vm is not
const { sel, mode, ctor, owner } = vnode;

const def = getComponentInternalDef(ctor);
createVM(elm, def, {
mode,
owner,
tagName: sel,
renderer: owner.renderer,
});

vnode.elm = elm as Element;

const vm = getAssociatedVM(elm);
allocateChildrenHook(vnode, vm);

hydrateElmHook(vnode);

// Insert hook section:
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(vm.state === VMState.created, `${vm} cannot be recycled.`);
}
runConnectedCallback(vm);

if (vm.renderMode !== RenderMode.Light) {
// VM is not rendering in Light DOM, we can proceed and hydrate the slotted content.
// Note: for Light DOM, this is handled while hydrating the VM
hydrateChildrenHook(vnode.elm.childNodes, vnode.children, vm);
}

hydrateVM(vm);
},
};

function linkNodeToShadow(elm: Node, owner: VM) {
Expand Down
Loading