Skip to content
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) => {
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