Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
57 changes: 56 additions & 1 deletion packages/@lwc/ssr-compiler/src/compile-template/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { ImportManager } from '../imports';
import type { ImportDeclaration as EsImportDeclaration } from 'estree';
import type { ImportDeclaration as EsImportDeclaration, Statement as EsStatement } from 'estree';
import type { TemplateOpts, TransformerContext } from './types';

export function createNewContext(templateOptions: TemplateOpts): {
Expand All @@ -33,14 +33,69 @@ export function createNewContext(templateOptions: TemplateOpts): {
}
return false;
};
const getLocalVars = () => localVarStack.flatMap((stackFrameVars) => [...stackFrameVars]);

const hoistedStatements = {
module: [] as EsStatement[],
templateFn: [] as EsStatement[],
};
const previouslyHoistedStatementKeysMod = new Set<unknown>();
const previouslyHoistedStatementKeysTmpl = new Set<unknown>();

const hoist = {
module(stmt: EsStatement, optionalDedupeKey?: unknown) {
if (optionalDedupeKey) {
if (previouslyHoistedStatementKeysMod.has(optionalDedupeKey)) {
return;
}
previouslyHoistedStatementKeysMod.add(optionalDedupeKey);
}
hoistedStatements.module.push(stmt);
},
templateFn(stmt: EsStatement, optionalDedupeKey?: unknown) {
if (optionalDedupeKey) {
if (previouslyHoistedStatementKeysTmpl.has(optionalDedupeKey)) {
return;
}
previouslyHoistedStatementKeysTmpl.add(optionalDedupeKey);
}
hoistedStatements.templateFn.push(stmt);
},
};

const shadowSlotToFnName = new Map<string, string>();
let fnNameUniqueId = 0;

const slots = {
shadow: {
isDuplicate(uniqueNodeId: string) {
return shadowSlotToFnName.has(uniqueNodeId);
},
register(uniqueNodeId: string, kebabCmpName: string) {
if (slots.shadow.isDuplicate(uniqueNodeId)) {
return shadowSlotToFnName.get(uniqueNodeId)!;
}
const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`;
shadowSlotToFnName.set(uniqueNodeId, shadowSlotContentFnName);
return shadowSlotContentFnName;
},
getFnName(uniqueNodeId: string) {
return shadowSlotToFnName.get(uniqueNodeId) ?? null;
},
},
};

return {
getImports: () => importManager.getImportDeclarations(),
cxt: {
pushLocalVars,
popLocalVars,
isLocalVar,
getLocalVars,
templateOptions,
hoist,
hoistedStatements,
slots,
import: importManager.add.bind(importManager),
siblings: undefined,
currentNodeIndex: undefined,
Expand Down
8 changes: 5 additions & 3 deletions packages/@lwc/ssr-compiler/src/compile-template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default function compileTemplate(
)?.value?.value;
const experimentalComplexExpressions = Boolean(options.experimentalComplexExpressions);

const { addImport, getImports, statements } = templateIrToEsTree(root, {
const { addImport, getImports, statements, cxt } = templateIrToEsTree(root, {
preserveComments,
experimentalComplexExpressions,
});
Expand All @@ -125,7 +125,9 @@ export default function compileTemplate(
addImport(imports, source);
}

let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts(statements));
let tmplDecl = bExportTemplate(
optimizeAdjacentYieldStmts([...cxt.hoistedStatements.templateFn, ...statements])
);
// Ideally, we'd just do ${LWC_VERSION_COMMENT} in the code template,
// but placeholders have a special meaning for `esTemplate`.
tmplDecl = produce(tmplDecl, (draft) => {
Expand All @@ -137,7 +139,7 @@ export default function compileTemplate(
];
});

let program = b.program([...getImports(), tmplDecl], 'module');
let program = b.program([...getImports(), ...cxt.hoistedStatements.module, tmplDecl], 'module');

addScopeTokenDeclarations(program, filename, options.namespace, options.name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,6 @@ export function templateIrToEsTree(node: IrNode, contextOpts: TemplateOpts) {
addImport: cxt.import,
getImports,
statements,
cxt,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@

import { produce } from 'immer';
import { builders as b, is } from 'estree-toolkit';
import { kebabCaseToCamelCase } from '@lwc/shared';
import { bAttributeValue, optimizeAdjacentYieldStmts } from '../../shared';
import { esTemplate, esTemplateWithYield } from '../../../estemplate';
import { irChildrenToEs, irToEs } from '../../ir-to-es';
import { isLiteral } from '../../shared';
import { expressionIrToEs } from '../../expression';
import { isNullableOf } from '../../../estree/validators';
import { isLastConcatenatedNode } from '../../adjacent-text-nodes';
import type { CallExpression as EsCallExpression, Expression as EsExpression } from 'estree';

import type {
CallExpression as EsCallExpression,
Expression as EsExpression,
Statement as EsStatement,
ExpressionStatement as EsExpressionStatement,
VariableDeclaration as EsVariableDeclaration,
} from 'estree';
import type {
ChildNode as IrChildNode,
Expand All @@ -36,51 +39,51 @@ import type {
} from '@lwc/template-compiler';
import type { TransformerContext } from '../../types';

const bGenerateSlottedContent = esTemplateWithYield`
const shadowSlottedContent = ${/* hasShadowSlottedContent */ is.literal}
? async function* __lwcGenerateSlottedContent(contextfulParent) {
// The 'contextfulParent' variable is shadowed here so that a contextful relationship
// is established between components rendered in slotted content & the "parent"
// component that contains the <slot>.

${/* shadow slot content */ is.statement}
}
// Avoid creating the object unnecessarily
: null;

const lightSlottedContentMap = ${/* hasLightSlottedContent */ is.literal}
? Object.create(null)
// Avoid creating the object unnecessarily
: null;

// The containing slot treats scoped slotted content differently.
const scopedSlottedContentMap = ${/* hasScopedSlottedContent */ is.literal}
? Object.create(null)
// Avoid creating the object unnecessarily
: null;

function addSlottedContent(name, fn, contentMap) {
let contentList = contentMap[name];
if (contentList) {
contentList.push(fn);
} else {
contentMap[name] = [fn];
}
}
const bGenerateShadowSlottedContent = esTemplateWithYield`
const ${/* function name */ is.identifier} = (${/* local vars */ is.identifier}) => async function* ${/* function name */ 0}(contextfulParent) {
// The 'contextfulParent' variable is shadowed here so that a contextful relationship
// is established between components rendered in slotted content & the "parent"
// component that contains the <slot>.
${/* shadow slot content */ is.statement}
};
`<EsVariableDeclaration>;
const bGenerateShadowSlottedContentRef = esTemplateWithYield`
const shadowSlottedContent = ${/* reference to hoisted fn */ is.identifier}(${/* local vars */ is.identifier});
`<EsVariableDeclaration>;
const bNullishGenerateShadowSlottedContent = esTemplateWithYield`
const shadowSlottedContent = null;
`<EsVariableDeclaration>;

${/* light DOM addLightContent statements */ is.expressionStatement}
${/* scoped slot addLightContent statements */ is.expressionStatement}
const bContentMap = esTemplateWithYield`
const ${/* name of the content map */ is.identifier} = Object.create(null);
`<EsVariableDeclaration>;
const bNullishContentMap = esTemplateWithYield`
const ${/* name of the content map */ is.identifier} = null;
`<EsVariableDeclaration>;

const bGenerateSlottedContent = esTemplateWithYield`
${/* const shadowSlottedContent = ... */ is.variableDeclaration}
${/* const lightSlottedContentMap */ is.variableDeclaration}
${/* const scopedSlottedContentMap */ is.variableDeclaration}
${/* light DOM addLightContent statements */ is.expressionStatement}
${/* scoped slot addLightContent statements */ is.expressionStatement}
`<EsStatement[]>;

// Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though
// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
// than a function _declaration_, so it isn't available to be referenced anywhere.
const bAddSlottedContent = esTemplate`
addSlottedContent(${/* slot name */ is.expression} ?? "", async function* __lwcGenerateSlottedContent(contextfulParent, ${
/* scoped slot data variable */ isNullableOf(is.identifier)
}, slotAttributeValue) {
${/* slot content */ is.statement}
}, ${/* content map */ is.identifier});
addSlottedContent(
${/* slot name */ is.expression} ?? "",
async function* __lwcGenerateSlottedContent(
contextfulParent,
${/* scoped slot data variable */ isNullableOf(is.identifier)},
slotAttributeValue)
{
${/* slot content */ is.statement}
},
${/* content map */ is.identifier}
);
`<EsCallExpression>;

function getShadowSlottedContent(slottableChildren: IrChildNode[], cxt: TransformerContext) {
Expand Down Expand Up @@ -155,7 +158,7 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex
});
const { isSlotted: originalIsSlotted } = cxt;
cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
const slotContent = irToEs(clone, cxt);
const slotContent = optimizeAdjacentYieldStmts(irToEs(clone, cxt));
cxt.isSlotted = originalIsSlotted;
results.push(
b.expressionStatement(
Expand Down Expand Up @@ -246,24 +249,65 @@ export function getSlottedContent(
bAddSlottedContent(
slotName,
boundVariable,
irChildrenToEs(child.children, cxt),
optimizeAdjacentYieldStmts(irChildrenToEs(child.children, cxt)),
b.identifier('scopedSlottedContentMap')
)
);
cxt.popLocalVars();
return addLightContentExpr;
});

const hasShadowSlottedContent = b.literal(shadowSlotContent.length > 0);
const hasLightSlottedContent = b.literal(lightSlotContent.length > 0);
const hasScopedSlottedContent = b.literal(scopedSlotContent.length > 0);
const hasShadowSlottedContent = shadowSlotContent.length > 0;
const hasLightSlottedContent = lightSlotContent.length > 0;
const hasScopedSlottedContent = scopedSlotContent.length > 0;
cxt.isSlotted = isSlotted;

if (hasShadowSlottedContent || hasLightSlottedContent || hasScopedSlottedContent) {
cxt.import('addSlottedContent');
}

// Elsewhere, nodes and their subtrees are cloned. This design decision means that
// the node objects themselves cannot be used as unique identifiers (e.g. as keys
// in a map). However, for a given template, a node's location information does
// uniquely identify that node.
const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`;

const localVars = cxt.getLocalVars();
const localVarIds = localVars.map(b.identifier);

if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) {
// Colon characters in <lwc:component> element name will result in an invalid
// JavaScript identifier if not otherwise accounted for.
const kebabCmpName = kebabCaseToCamelCase(node.name).replace(':', '_');
const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName);
const shadowSlottedContentFn = bGenerateShadowSlottedContent(
b.identifier(shadowSlotContentFnName),
// If the slot-fn were defined here instead of hoisted to the top of the module,
// the local variables (e.g. from for:each) would be closed-over. When hoisted,
// however, we need to curry these variables.
localVarIds,
shadowSlotContent
);
cxt.hoist.templateFn(shadowSlottedContentFn, node);
}

const shadowSlottedContentFn = hasShadowSlottedContent
? bGenerateShadowSlottedContentRef(
b.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)!),
localVarIds
)
: bNullishGenerateShadowSlottedContent();
const lightSlottedContentMap = hasLightSlottedContent
? bContentMap(b.identifier('lightSlottedContentMap'))
: bNullishContentMap(b.identifier('lightSlottedContentMap'));
const scopedSlottedContentMap = hasScopedSlottedContent
? bContentMap(b.identifier('scopedSlottedContentMap'))
: bNullishContentMap(b.identifier('scopedSlottedContentMap'));

return bGenerateSlottedContent(
hasShadowSlottedContent,
shadowSlotContent,
hasLightSlottedContent,
hasScopedSlottedContent,
shadowSlottedContentFn,
lightSlottedContentMap,
scopedSlottedContentMap,
lightSlotContent,
scopedSlotContent
);
Expand Down
18 changes: 18 additions & 0 deletions packages/@lwc/ssr-compiler/src/compile-template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,32 @@ export type Transformer<T extends IrNode = IrNode> = (
cxt: TransformerContext
) => EsStatement[];

export interface SlotMetadataContext {
shadow: {
isDuplicate: (uniqueNodeId: string) => boolean;
register: (uniqueNodeId: string, kebabCmpName: string) => string;
getFnName: (uniqueNodeId: string) => string | null;
};
}

export interface TransformerContext {
pushLocalVars: (vars: string[]) => void;
popLocalVars: () => void;
isLocalVar: (varName: string | null | undefined) => boolean;
getLocalVars: () => string[];
templateOptions: TemplateOpts;
siblings: IrNode[] | undefined;
currentNodeIndex: number | undefined;
isSlotted?: boolean;
hoistedStatements: {
module: EsStatement[];
templateFn: EsStatement[];
};
hoist: {
module: (stmt: EsStatement, optionalDedupeKey?: unknown) => void;
templateFn: (stmt: EsStatement, optionalDedupeKey?: unknown) => void;
};
slots: SlotMetadataContext;
import: (
imports: string | string[] | Record<string, string | undefined>,
source?: string
Expand Down
49 changes: 0 additions & 49 deletions packages/@lwc/ssr-compiler/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,6 @@ import type { Config as TemplateCompilerConfig } from '@lwc/template-compiler';

export type Expression = string;

export type Instruction =
| IEmitTagName
| IEmitStaticString
| IEmitExpression
| IStartConditional
| IEndConditional
| IInvokeConnectedCallback
| IRenderChild
| IHoistImport
| IHoistInstantiation;

export interface IEmitTagName {
kind: 'emitTagName';
}

export interface IEmitStaticString {
kind: 'emitStaticString';
}

export interface IEmitExpression {
kind: 'emitExpression';
expression: Expression;
}

export interface IStartConditional {
kind: 'startConditional';
}

export interface IEndConditional {
kind: 'endConditional';
}

export interface IInvokeConnectedCallback {
kind: 'invokeConnectedCallback';
}

export interface IRenderChild {
kind: 'renderChild';
dynamic: Expression | null;
}

export interface IHoistImport {
kind: 'hoistImport';
}

export interface IHoistInstantiation {
kind: 'hoistInstantiation';
}

export type TemplateTransformOptions = Pick<TemplateCompilerConfig, 'name' | 'namespace'>;
export type ComponentTransformOptions = Partial<
Pick<LwcBabelPluginOptions, 'name' | 'namespace'>
Expand Down
Loading