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

fix(ssr): more fixes for light DOM slots #4695

Merged
merged 22 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
7 changes: 3 additions & 4 deletions packages/@lwc/ssr-compiler/src/compile-template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ const bExportTemplate = esTemplate`

if (!isLightDom) {
yield '</template>';
}

if (slottedContent) {
yield* slottedContent();
if (slottedContent?.shadow) {
yield* slottedContent.shadow();
}
}
}
`<EsExportDefaultDeclaration>;
Expand Down
24 changes: 22 additions & 2 deletions packages/@lwc/ssr-compiler/src/compile-template/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { is } from 'estree-toolkit';
import { builders as b, is } from 'estree-toolkit';
import { reservedKeywords } from '@lwc/shared';
import { Node as IrNode } from '@lwc/template-compiler';
import { esTemplate } from '../estemplate';

import type { ImportDeclaration as EsImportDeclaration, Statement as EsStatement } from 'estree';
import type {
ImportDeclaration as EsImportDeclaration,
Statement as EsStatement,
Expression as EsExpression,
} from 'estree';

export const bImportHtmlEscape = esTemplate`
import { htmlEscape } from '@lwc/shared';
Expand Down Expand Up @@ -55,3 +60,18 @@ export function optimizeAdjacentYieldStmts(statements: EsStatement[]): EsStateme
})
.filter((el): el is NonNullable<EsStatement> => el !== null);
}

export function bAttributeValue(node: IrNode, attrName: string): EsExpression {
if (!('attributes' in node)) {
throw new TypeError(`Cannot get attribute value from ${node.type}`);
}
const nameAttrValue = node.attributes.find((attr) => attr.name === attrName)?.value;
if (!nameAttrValue) {
return b.literal(null);
} else if (nameAttrValue.type === 'Literal') {
const name = typeof nameAttrValue.value === 'string' ? nameAttrValue.value : '';
return b.literal(name);
} else {
return b.memberExpression(b.literal('instance'), nameAttrValue as EsExpression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { produce } from 'immer';
import { builders as b, is } from 'estree-toolkit';
import { kebabcaseToCamelcase, toPropertyName } from '@lwc/template-compiler';
import { normalizeStyleAttribute } from '@lwc/shared';
import { esTemplateWithYield } from '../../estemplate';
import { isValidIdentifier, optimizeAdjacentYieldStmts } from '../shared';
import { esTemplate, esTemplateWithYield } from '../../estemplate';
import { bAttributeValue, isValidIdentifier, optimizeAdjacentYieldStmts } from '../shared';
import { TransformerContext } from '../types';
import { expressionIrToEs } from '../expression';
import { irChildrenToEs } from '../ir-to-es';
import { irChildrenToEs, irToEs } from '../ir-to-es';
import type { CallExpression as EsCallExpression } from 'estree';

import type {
BlockStatement as EsBlockStatement,
Expand All @@ -29,13 +31,31 @@ const bYieldFromChildGenerator = esTemplateWithYield`
{
const childProps = ${is.objectExpression};
const childAttrs = ${is.objectExpression};
async function* childSlottedContentGenerator() {
${is.statement}
const slottedContent = {
light: Object.create(null),
shadow: async function* () {
${/* shadow slot content */ is.statement}
}
};
yield* ${is.identifier}(${is.literal}, childProps, childAttrs, childSlottedContentGenerator);
function addContent(name, fn) {
let contentList = slottedContent.light[name]
if (contentList) {
contentList.push(fn)
} else {
slottedContent.light[name] = [fn]
}
}
${/* addContent statements */ is.callExpression}
yield* ${is.identifier}(${is.literal}, childProps, childAttrs, slottedContent);
Comment on lines +34 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

Future optimization: dedupe content by changing slottedContent.shadow to be an array and add slottedContent.shadow.push(fn) in addContent. This will require updating the content of the generated functions to conditionally render the slot attribute.

}
`<EsBlockStatement>;

const bAddContent = esTemplate`
addContent(${/* slot name */ is.expression} ?? "", async function* () {
${/* slot content */ is.statement}
});
`<EsCallExpression>;

const bImportGenerateMarkup = (localName: string, importPath: string) =>
b.importDeclaration(
[b.importSpecifier(b.identifier('generateMarkup'), b.identifier(localName))],
Expand Down Expand Up @@ -95,11 +115,29 @@ export const Component: Transformer<IrComponent> = function Component(node, cxt)

const attributes = [...node.attributes, ...reflectAriaPropsAsAttrs(node.properties)];

const shadowSlotContent = optimizeAdjacentYieldStmts(irChildrenToEs(node.children, cxt));

const lightSlotContent = node.children.map((child) => {
if ('attributes' in child) {
const slotName = bAttributeValue(child, 'slot');
// FIXME: We don't know what happens for slot attributes inside an lwc:if block
// Light DOM slots do not actually render the `slot` attribute.
const clone = produce(child, (draft) => {
nolanlawson marked this conversation as resolved.
Show resolved Hide resolved
draft.attributes = draft.attributes.filter((attr) => attr.name !== 'slot');
});
const slotContent = irToEs(clone, cxt);
return bAddContent(slotName, slotContent);
} else {
return bAddContent(b.literal(''), irToEs(child, cxt));
}
});

return [
bYieldFromChildGenerator(
getChildAttrsOrProps(node.properties, cxt),
getChildAttrsOrProps(attributes, cxt),
optimizeAdjacentYieldStmts(irChildrenToEs(node.children, cxt)),
shadowSlotContent,
lightSlotContent,
b.identifier(childGeneratorLocalName),
b.literal(childTagName)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,27 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { builders as b, is } from 'estree-toolkit';
import { is } from 'estree-toolkit';

import { Slot as IrSlot } from '@lwc/template-compiler';
import { esTemplateWithYield } from '../../estemplate';

import { irChildrenToEs } from '../ir-to-es';
import { bAttributeValue } from '../shared';
import { Element } from './element';
import type {
Expression as EsExpression,
Statement as EsStatement,
IfStatement as EsIfStatement,
} from 'estree';
import type { Statement as EsStatement, IfStatement as EsIfStatement } from 'estree';
import type { Transformer } from '../types';

const bConditionalSlot = esTemplateWithYield`
if (isLightDom) {
// start bookend HTML comment
yield '<!---->';

const generator = slottedContent[${/* slotName */ is.expression} ?? ""];
if (generator) {
yield* generator();
const generators = slottedContent?.light[${/* slotName */ is.expression} ?? ""];
if (generators) {
for (const generator of generators) {
yield* generator();
}
} else {
// If we're in this else block, then the generator _must_ have yielded
// something. It's impossible for a slottedContent["foo"] to exist
Expand All @@ -44,21 +43,9 @@ const bConditionalSlot = esTemplateWithYield`
`<EsIfStatement>;

export const Slot: Transformer<IrSlot> = function Slot(node, ctx): EsStatement[] {
const nameAttrValue = node.attributes.find((attr) => attr.name === 'name')?.value;
let slotName: EsExpression;
if (!nameAttrValue) {
slotName = b.literal('');
} else if (nameAttrValue.type === 'Literal') {
const name = typeof nameAttrValue.value === 'string' ? nameAttrValue.value : '';
slotName = b.literal(name);
} else {
slotName = b.memberExpression(b.literal('instance'), nameAttrValue as EsExpression);
}

const slotName = bAttributeValue(node, 'name');
// FIXME: avoid serializing the slot's children twice
const slotAst = Element(node, ctx);

const slotChildren = irChildrenToEs(node.children, ctx);

return [bConditionalSlot(slotName, slotChildren, slotAst)];
};
14 changes: 6 additions & 8 deletions packages/@lwc/ssr-compiler/src/estemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ interface TraversalState {

const getReplacementNode = (
state: TraversalState,
placeholderId: string,
nodeType: string
placeholderId: string
): EsNode | EsNode[] | null => {
const key = Number(placeholderId.slice(PLACEHOLDER_PREFIX.length));
const nodeCount = state.replacementNodes.length;
Expand All @@ -92,6 +91,9 @@ const getReplacementNode = (
? replacementNode.every(validateReplacement)
: validateReplacement(replacementNode))
) {
const nodeType = Array.isArray(replacementNode)
? `[${replacementNode.map((n) => n.type)}.join(', ')]`
: replacementNode?.type;
throw new Error(`Validation failed for templated node of type ${nodeType}`);
}

Expand All @@ -101,7 +103,7 @@ const getReplacementNode = (
const visitors: Visitors<TraversalState> = {
Identifier(path, state) {
if (path.node?.name.startsWith(PLACEHOLDER_PREFIX)) {
const replacementNode = getReplacementNode(state, path.node.name, path.node.type);
const replacementNode = getReplacementNode(state, path.node.name);

if (replacementNode === null) {
path.remove();
Expand All @@ -126,11 +128,7 @@ const visitors: Visitors<TraversalState> = {
path.node.value.startsWith(PLACEHOLDER_PREFIX)
) {
// A literal can only be replaced with a single node
const replacementNode = getReplacementNode(
state,
path.node.value,
path.node.type
) as EsNode;
const replacementNode = getReplacementNode(state, path.node.value) as EsNode;

path.replaceWith(replacementNode);
}
Expand Down
Loading