From f902c162b33478460bcc6f1ed16ad538203194cc Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Fri, 16 May 2025 17:38:49 -0700 Subject: [PATCH 01/15] chore(ssr): move some calculations to compile-time for sleeker output --- .../transformers/component/slotted-content.ts | 86 ++++++++++--------- packages/@lwc/ssr-runtime/src/index.ts | 1 + packages/@lwc/ssr-runtime/src/render.ts | 13 +++ 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 50f2c890de..16cc826566 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -14,11 +14,13 @@ 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, @@ -36,40 +38,31 @@ 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 . - - ${/* shadow slot content */ is.statement} - } - // Avoid creating the object unnecessarily - : null; +const bGenerateShadowSlottedContent = esTemplateWithYield` + const shadowSlottedContent = 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 . + ${/* shadow slot content */ is.statement} + }; +`; +const bNullishGenerateShadowSlottedContent = esTemplateWithYield` + const shadowSlottedContent = 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; +const bContentMap = esTemplateWithYield` + const ${/* name of the content map */ is.identifier} = Object.create(null); +`; +const bNullishContentMap = esTemplateWithYield` + const ${/* name of the content map */ is.identifier} = null; +`; - function addSlottedContent(name, fn, contentMap) { - let contentList = contentMap[name]; - if (contentList) { - contentList.push(fn); - } else { - contentMap[name] = [fn]; - } - } - - ${/* light DOM addLightContent statements */ is.expressionStatement} - ${/* scoped slot addLightContent statements */ is.expressionStatement} +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} `; // Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though @@ -254,16 +247,29 @@ export function getSlottedContent( 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'); + } + + const shadowSlottedContentFn = hasShadowSlottedContent + ? bGenerateShadowSlottedContent(shadowSlotContent) + : 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 ); diff --git a/packages/@lwc/ssr-runtime/src/index.ts b/packages/@lwc/ssr-runtime/src/index.ts index d2537bd90f..c65c028725 100644 --- a/packages/@lwc/ssr-runtime/src/index.ts +++ b/packages/@lwc/ssr-runtime/src/index.ts @@ -31,6 +31,7 @@ export { GenerateMarkupAsyncYield, renderAttrs, renderAttrsNoYield, + addSlottedContent, serverSideRenderComponent, // renderComponent is an alias for serverSideRenderComponent serverSideRenderComponent as renderComponent, diff --git a/packages/@lwc/ssr-runtime/src/render.ts b/packages/@lwc/ssr-runtime/src/render.ts index 8b631a52f8..f108286db7 100644 --- a/packages/@lwc/ssr-runtime/src/render.ts +++ b/packages/@lwc/ssr-runtime/src/render.ts @@ -187,6 +187,19 @@ export function fallbackTmplNoYield( } } +export function addSlottedContent( + name: string, + fn: unknown, + contentMap: Record +) { + const contentList = contentMap[name]; + if (contentList) { + contentList.push(fn); + } else { + contentMap[name] = [fn]; + } +} + interface ComponentWithGenerateMarkup extends LightningElementConstructor { [SYMBOL__GENERATE_MARKUP]?: GenerateMarkupVariants; } From a167b6a7e0da64da3d4d7c09b73c851ad3157038 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 20 May 2025 11:34:36 -0700 Subject: [PATCH 02/15] chore: remove unused types --- packages/@lwc/ssr-compiler/src/shared.ts | 49 ------------------------ 1 file changed, 49 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/shared.ts b/packages/@lwc/ssr-compiler/src/shared.ts index cd6a390a2a..f5c437fd77 100644 --- a/packages/@lwc/ssr-compiler/src/shared.ts +++ b/packages/@lwc/ssr-compiler/src/shared.ts @@ -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; export type ComponentTransformOptions = Partial< Pick From 918f0ce7cf8b26b7a2a77afb0c3bb61dc0aa2707 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Thu, 22 May 2025 16:30:52 -0700 Subject: [PATCH 03/15] chore: add mechanism to hoist statements --- .../ssr-compiler/src/compile-template/context.ts | 16 +++++++++++++++- .../ssr-compiler/src/compile-template/index.ts | 4 ++-- .../src/compile-template/ir-to-es.ts | 1 + .../ssr-compiler/src/compile-template/types.ts | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index 6de3a6b23c..100adc2a54 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -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): { @@ -34,6 +34,18 @@ export function createNewContext(templateOptions: TemplateOpts): { return false; }; + const hoistedStatements: EsStatement[] = []; + const previouslyHoistedStatementKeys = new Set(); + const hoist = (stmt: EsStatement, optionalDedupeKey?: unknown) => { + if (optionalDedupeKey) { + if (previouslyHoistedStatementKeys.has(optionalDedupeKey)) { + return; + } + previouslyHoistedStatementKeys.add(optionalDedupeKey); + } + hoistedStatements.push(stmt); + }; + return { getImports: () => importManager.getImportDeclarations(), cxt: { @@ -41,6 +53,8 @@ export function createNewContext(templateOptions: TemplateOpts): { popLocalVars, isLocalVar, templateOptions, + hoist, + hoistedStatements, import: importManager.add.bind(importManager), siblings: undefined, currentNodeIndex: undefined, diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index 74cc04092a..23a44822ef 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -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, }); @@ -137,7 +137,7 @@ export default function compileTemplate( ]; }); - let program = b.program([...getImports(), tmplDecl], 'module'); + let program = b.program([...getImports(), ...cxt.hoistedStatements, tmplDecl], 'module'); addScopeTokenDeclarations(program, filename, options.namespace, options.name); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/ir-to-es.ts b/packages/@lwc/ssr-compiler/src/compile-template/ir-to-es.ts index 8d5fc027f9..76dd5f42a6 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/ir-to-es.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/ir-to-es.ts @@ -110,5 +110,6 @@ export function templateIrToEsTree(node: IrNode, contextOpts: TemplateOpts) { addImport: cxt.import, getImports, statements, + cxt, }; } diff --git a/packages/@lwc/ssr-compiler/src/compile-template/types.ts b/packages/@lwc/ssr-compiler/src/compile-template/types.ts index f9cd8adcea..0109dfd19d 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/types.ts @@ -21,6 +21,8 @@ export interface TransformerContext { siblings: IrNode[] | undefined; currentNodeIndex: number | undefined; isSlotted?: boolean; + hoistedStatements: EsStatement[]; + hoist: (stmt: EsStatement, optionalDedupeKey?: unknown) => void; import: ( imports: string | string[] | Record, source?: string From f350e080f301a5ef6296311b55281b55a464a47a Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Thu, 22 May 2025 17:49:36 -0700 Subject: [PATCH 04/15] wip: hoist shadow slot fns --- .../transformers/component/slotted-content.ts | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 16cc826566..2203748cce 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -7,6 +7,7 @@ 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'; @@ -20,6 +21,7 @@ import type { Expression as EsExpression, Statement as EsStatement, ExpressionStatement as EsExpressionStatement, + FunctionDeclaration as EsFunctionDeclaration, VariableDeclaration as EsVariableDeclaration, } from 'estree'; import type { @@ -38,13 +40,19 @@ import type { } from '@lwc/template-compiler'; import type { TransformerContext } from '../../types'; +const slotAttributeValueAssignment = + esTemplate`const slotAttributeValue = null;`(); + const bGenerateShadowSlottedContent = esTemplateWithYield` - const shadowSlottedContent = async function* __lwcGenerateSlottedContent(contextfulParent) { + async function* ${/* function name */ is.identifier}(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 . ${/* shadow slot content */ is.statement} - }; + } +`; +const bGenerateShadowSlottedContentRef = esTemplateWithYield` + const shadowSlottedContent = ${/* reference to hoisted fn */ is.identifier}; `; const bNullishGenerateShadowSlottedContent = esTemplateWithYield` const shadowSlottedContent = null; @@ -69,11 +77,17 @@ const bGenerateSlottedContent = esTemplateWithYield` // 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} + ); `; function getShadowSlottedContent(slottableChildren: IrChildNode[], cxt: TransformerContext) { @@ -148,7 +162,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( @@ -207,6 +221,11 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex return results; } +// gonna have to attach this to compilation context so that we don't have collisions +// when multiple compilations happen in the same process +const slotToShadowSlotFnName = new Map(); +let fnNameUniqueId = 0; + export function getSlottedContent( node: IrLwcComponent | IrComponent, cxt: TransformerContext @@ -239,7 +258,7 @@ export function getSlottedContent( bAddSlottedContent( slotName, boundVariable, - irChildrenToEs(child.children, cxt), + optimizeAdjacentYieldStmts(irChildrenToEs(child.children, cxt)), b.identifier('scopedSlottedContentMap') ) ); @@ -256,8 +275,23 @@ export function getSlottedContent( cxt.import('addSlottedContent'); } + const kebabCmpName = kebabCaseToCamelCase(node.name); + const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`; + + if (hasShadowSlottedContent && !slotToShadowSlotFnName.has(uniqueNodeId)) { + cxt.hoist(slotAttributeValueAssignment, slotAttributeValueAssignment); + + const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`; + slotToShadowSlotFnName.set(uniqueNodeId, shadowSlotContentFnName); + const shadowSlottedContentFn = bGenerateShadowSlottedContent( + b.identifier(shadowSlotContentFnName), + shadowSlotContent + ); + cxt.hoist(shadowSlottedContentFn, node); + } + const shadowSlottedContentFn = hasShadowSlottedContent - ? bGenerateShadowSlottedContent(shadowSlotContent) + ? bGenerateShadowSlottedContentRef(b.identifier(slotToShadowSlotFnName.get(uniqueNodeId)!)) : bNullishGenerateShadowSlottedContent(); const lightSlottedContentMap = hasLightSlottedContent ? bContentMap(b.identifier('lightSlottedContentMap')) From d24b4020102d959b564b34ee320002431ba343da Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Thu, 22 May 2025 22:06:02 -0700 Subject: [PATCH 05/15] wip: address initial issues with hoisted shadow slot fn --- .../src/compile-template/context.ts | 23 +++++++++++++++++++ .../src/compile-template/index.ts | 2 +- .../transformers/component/slotted-content.ts | 17 ++++---------- .../src/compile-template/types.ts | 9 ++++++++ .../@lwc/ssr-compiler/src/transmogrify.ts | 2 +- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index 100adc2a54..bad77b266e 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -46,6 +46,28 @@ export function createNewContext(templateOptions: TemplateOpts): { hoistedStatements.push(stmt); }; + const shadowSlotToFnName = new Map(); + 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: { @@ -55,6 +77,7 @@ export function createNewContext(templateOptions: TemplateOpts): { templateOptions, hoist, hoistedStatements, + slots, import: importManager.add.bind(importManager), siblings: undefined, currentNodeIndex: undefined, diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index 23a44822ef..a939db12f2 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -68,7 +68,7 @@ const bExportTemplate = esTemplate` if (shadowSlottedContent) { // instance must be passed in; this is used to establish the contextful relationship // between context provider (aka parent component) and context consumer (aka slotted content) - yield* shadowSlottedContent(contextfulParent); + yield* shadowSlottedContent(contextfulParent, Cmp); } } } diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 2203748cce..c1be7d99dd 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -44,7 +44,7 @@ const slotAttributeValueAssignment = esTemplate`const slotAttributeValue = null;`(); const bGenerateShadowSlottedContent = esTemplateWithYield` - async function* ${/* function name */ is.identifier}(contextfulParent) { + async function* ${/* function name */ is.identifier}(contextfulParent, Cmp) { // 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 . @@ -221,11 +221,6 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex return results; } -// gonna have to attach this to compilation context so that we don't have collisions -// when multiple compilations happen in the same process -const slotToShadowSlotFnName = new Map(); -let fnNameUniqueId = 0; - export function getSlottedContent( node: IrLwcComponent | IrComponent, cxt: TransformerContext @@ -275,14 +270,12 @@ export function getSlottedContent( cxt.import('addSlottedContent'); } - const kebabCmpName = kebabCaseToCamelCase(node.name); const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`; - if (hasShadowSlottedContent && !slotToShadowSlotFnName.has(uniqueNodeId)) { + if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) { cxt.hoist(slotAttributeValueAssignment, slotAttributeValueAssignment); - - const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`; - slotToShadowSlotFnName.set(uniqueNodeId, shadowSlotContentFnName); + const kebabCmpName = kebabCaseToCamelCase(node.name); + const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName); const shadowSlottedContentFn = bGenerateShadowSlottedContent( b.identifier(shadowSlotContentFnName), shadowSlotContent @@ -291,7 +284,7 @@ export function getSlottedContent( } const shadowSlottedContentFn = hasShadowSlottedContent - ? bGenerateShadowSlottedContentRef(b.identifier(slotToShadowSlotFnName.get(uniqueNodeId)!)) + ? bGenerateShadowSlottedContentRef(b.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)!)) : bNullishGenerateShadowSlottedContent(); const lightSlottedContentMap = hasLightSlottedContent ? bContentMap(b.identifier('lightSlottedContentMap')) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/types.ts b/packages/@lwc/ssr-compiler/src/compile-template/types.ts index 0109dfd19d..50ff6f6044 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/types.ts @@ -13,6 +13,14 @@ export type Transformer = ( 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; @@ -23,6 +31,7 @@ export interface TransformerContext { isSlotted?: boolean; hoistedStatements: EsStatement[]; hoist: (stmt: EsStatement, optionalDedupeKey?: unknown) => void; + slots: SlotMetadataContext; import: ( imports: string | string[] | Record, source?: string diff --git a/packages/@lwc/ssr-compiler/src/transmogrify.ts b/packages/@lwc/ssr-compiler/src/transmogrify.ts index 0ffb61b26c..10a19acc66 100644 --- a/packages/@lwc/ssr-compiler/src/transmogrify.ts +++ b/packages/@lwc/ssr-compiler/src/transmogrify.ts @@ -21,7 +21,7 @@ export type Visitors = Parameters { const { node } = nodePath; From 235d8f6563a9029728dde2c53da7b34f09bd79a7 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 14:08:15 -0700 Subject: [PATCH 06/15] wip: complete non-working solution --- packages/@lwc/ssr-compiler/src/compile-template/index.ts | 2 +- .../transformers/component/slotted-content.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index a939db12f2..c989c1cb52 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -68,7 +68,7 @@ const bExportTemplate = esTemplate` if (shadowSlottedContent) { // instance must be passed in; this is used to establish the contextful relationship // between context provider (aka parent component) and context consumer (aka slotted content) - yield* shadowSlottedContent(contextfulParent, Cmp); + yield* shadowSlottedContent(contextfulParent, Cmp, instance); } } } diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index c1be7d99dd..1060bc84f3 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -43,8 +43,12 @@ import type { TransformerContext } from '../../types'; const slotAttributeValueAssignment = esTemplate`const slotAttributeValue = null;`(); +// Toodles: rather than hoising this function to the top of the module, maybe it should be +// hoisted to the top of the template function. Many things would still be in +// scope that way, and it might not be quite so brittle. And it would still allow +// to dedupe the bullshit const bGenerateShadowSlottedContent = esTemplateWithYield` - async function* ${/* function name */ is.identifier}(contextfulParent, Cmp) { + async function* ${/* function name */ is.identifier}(contextfulParent, Cmp, instance) { // 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 . From 30a649eaa8a6576f391ab9131ca36782efed48f8 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 14:11:27 -0700 Subject: [PATCH 07/15] chore: comment --- .../transformers/component/slotted-content.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 1060bc84f3..36589eed44 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -274,6 +274,10 @@ export function getSlottedContent( 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, in a given template, the location information does uniquely + // identify a given node. const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`; if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) { From 6a408d2678791c06a0b634dd603390f307d6c1c9 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 16:25:23 -0700 Subject: [PATCH 08/15] wip: new approach with hoisting to top of tmpl fn --- .../src/compile-template/context.ts | 34 ++++++++++++++----- .../src/compile-template/index.ts | 8 +++-- .../transformers/component/slotted-content.ts | 14 ++++---- .../src/compile-template/types.ts | 10 ++++-- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index bad77b266e..4be68cd85d 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -34,16 +34,32 @@ export function createNewContext(templateOptions: TemplateOpts): { return false; }; - const hoistedStatements: EsStatement[] = []; - const previouslyHoistedStatementKeys = new Set(); - const hoist = (stmt: EsStatement, optionalDedupeKey?: unknown) => { - if (optionalDedupeKey) { - if (previouslyHoistedStatementKeys.has(optionalDedupeKey)) { - return; + const hoistedStatements = { + module: [] as EsStatement[], + templateFn: [] as EsStatement[], + }; + const previouslyHoistedStatementKeysMod = new Set(); + const previouslyHoistedStatementKeysTmpl = new Set(); + + const hoist = { + module(stmt: EsStatement, optionalDedupeKey?: unknown) { + if (optionalDedupeKey) { + if (previouslyHoistedStatementKeysMod.has(optionalDedupeKey)) { + return; + } + previouslyHoistedStatementKeysMod.add(optionalDedupeKey); } - previouslyHoistedStatementKeys.add(optionalDedupeKey); - } - hoistedStatements.push(stmt); + 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(); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index c989c1cb52..0500f8c820 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -68,7 +68,7 @@ const bExportTemplate = esTemplate` if (shadowSlottedContent) { // instance must be passed in; this is used to establish the contextful relationship // between context provider (aka parent component) and context consumer (aka slotted content) - yield* shadowSlottedContent(contextfulParent, Cmp, instance); + yield* shadowSlottedContent(contextfulParent); } } } @@ -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) => { @@ -137,7 +139,7 @@ export default function compileTemplate( ]; }); - let program = b.program([...getImports(), ...cxt.hoistedStatements, tmplDecl], 'module'); + let program = b.program([...getImports(), ...cxt.hoistedStatements.module, tmplDecl], 'module'); addScopeTokenDeclarations(program, filename, options.namespace, options.name); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 36589eed44..4ee6887926 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -40,15 +40,15 @@ import type { } from '@lwc/template-compiler'; import type { TransformerContext } from '../../types'; -const slotAttributeValueAssignment = - esTemplate`const slotAttributeValue = null;`(); +// const slotAttributeValueAssignment = +// esTemplate`const slotAttributeValue = null;`(); // Toodles: rather than hoising this function to the top of the module, maybe it should be // hoisted to the top of the template function. Many things would still be in // scope that way, and it might not be quite so brittle. And it would still allow // to dedupe the bullshit const bGenerateShadowSlottedContent = esTemplateWithYield` - async function* ${/* function name */ is.identifier}(contextfulParent, Cmp, instance) { + async function* ${/* function name */ is.identifier}(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 . @@ -276,19 +276,19 @@ export function getSlottedContent( // 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, in a given template, the location information does uniquely - // identify a given node. + // 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}`; if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) { - cxt.hoist(slotAttributeValueAssignment, slotAttributeValueAssignment); + // cxt.hoist.templateFn(slotAttributeValueAssignment, slotAttributeValueAssignment); const kebabCmpName = kebabCaseToCamelCase(node.name); const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName); const shadowSlottedContentFn = bGenerateShadowSlottedContent( b.identifier(shadowSlotContentFnName), shadowSlotContent ); - cxt.hoist(shadowSlottedContentFn, node); + cxt.hoist.templateFn(shadowSlottedContentFn, node); } const shadowSlottedContentFn = hasShadowSlottedContent diff --git a/packages/@lwc/ssr-compiler/src/compile-template/types.ts b/packages/@lwc/ssr-compiler/src/compile-template/types.ts index 50ff6f6044..e4ff810747 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/types.ts @@ -29,8 +29,14 @@ export interface TransformerContext { siblings: IrNode[] | undefined; currentNodeIndex: number | undefined; isSlotted?: boolean; - hoistedStatements: EsStatement[]; - hoist: (stmt: EsStatement, optionalDedupeKey?: unknown) => void; + 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, From da23cd049cbdc224994bbbb0542fcb6aadd42807 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 17:56:48 -0700 Subject: [PATCH 09/15] fix: invalid slot-fn identifier when lwc:component is used --- .../transformers/component/slotted-content.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 4ee6887926..ddc4ce9146 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -281,8 +281,9 @@ export function getSlottedContent( const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`; if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) { - // cxt.hoist.templateFn(slotAttributeValueAssignment, slotAttributeValueAssignment); - const kebabCmpName = kebabCaseToCamelCase(node.name); + // Colon characters in 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), From 580fc4cb8c2f6352a31cf747b849827a476edfb5 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 18:29:38 -0700 Subject: [PATCH 10/15] feat: curry local variables into the shadow slot content fn --- .../src/compile-template/context.ts | 2 ++ .../transformers/component/slotted-content.ts | 28 ++++++++++--------- .../src/compile-template/types.ts | 1 + 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index 4be68cd85d..7429d5c23b 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -33,6 +33,7 @@ export function createNewContext(templateOptions: TemplateOpts): { } return false; }; + const getLocalVars = () => localVarStack.flatMap((stackFrameVars) => [...stackFrameVars]); const hoistedStatements = { module: [] as EsStatement[], @@ -90,6 +91,7 @@ export function createNewContext(templateOptions: TemplateOpts): { pushLocalVars, popLocalVars, isLocalVar, + getLocalVars, templateOptions, hoist, hoistedStatements, diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index ddc4ce9146..e050e7b016 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -21,7 +21,6 @@ import type { Expression as EsExpression, Statement as EsStatement, ExpressionStatement as EsExpressionStatement, - FunctionDeclaration as EsFunctionDeclaration, VariableDeclaration as EsVariableDeclaration, } from 'estree'; import type { @@ -40,23 +39,16 @@ import type { } from '@lwc/template-compiler'; import type { TransformerContext } from '../../types'; -// const slotAttributeValueAssignment = -// esTemplate`const slotAttributeValue = null;`(); - -// Toodles: rather than hoising this function to the top of the module, maybe it should be -// hoisted to the top of the template function. Many things would still be in -// scope that way, and it might not be quite so brittle. And it would still allow -// to dedupe the bullshit const bGenerateShadowSlottedContent = esTemplateWithYield` - async function* ${/* function name */ is.identifier}(contextfulParent) { + 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 . ${/* shadow slot content */ is.statement} - } -`; + }; +`; const bGenerateShadowSlottedContentRef = esTemplateWithYield` - const shadowSlottedContent = ${/* reference to hoisted fn */ is.identifier}; + const shadowSlottedContent = ${/* reference to hoisted fn */ is.identifier}(${/* local vars */ is.identifier}); `; const bNullishGenerateShadowSlottedContent = esTemplateWithYield` const shadowSlottedContent = null; @@ -280,6 +272,9 @@ export function getSlottedContent( // 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 element name will result in an invalid // JavaScript identifier if not otherwise accounted for. @@ -287,13 +282,20 @@ export function getSlottedContent( 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)!)) + ? bGenerateShadowSlottedContentRef( + b.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)!), + localVarIds + ) : bNullishGenerateShadowSlottedContent(); const lightSlottedContentMap = hasLightSlottedContent ? bContentMap(b.identifier('lightSlottedContentMap')) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/types.ts b/packages/@lwc/ssr-compiler/src/compile-template/types.ts index e4ff810747..73f244a87d 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/types.ts @@ -25,6 +25,7 @@ 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; From 8cb3174330cfb0402f55daea52ae18e89b0860ea Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 23:29:37 -0700 Subject: [PATCH 11/15] chore: comments and some light cleanup --- .../src/compile-template/context.ts | 8 +++++++ .../src/compile-template/index.ts | 22 +++++++++++++++++-- .../transformers/component/slotted-content.ts | 19 +++++++++++----- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index 7429d5c23b..4425d5c4d3 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -43,6 +43,8 @@ export function createNewContext(templateOptions: TemplateOpts): { const previouslyHoistedStatementKeysTmpl = new Set(); const hoist = { + // Anything added here will be inserted at the top of the compiled template's + // JS module. module(stmt: EsStatement, optionalDedupeKey?: unknown) { if (optionalDedupeKey) { if (previouslyHoistedStatementKeysMod.has(optionalDedupeKey)) { @@ -52,6 +54,8 @@ export function createNewContext(templateOptions: TemplateOpts): { } hoistedStatements.module.push(stmt); }, + // Anything added here will be inserted at the top of the JavaScript function + // corresponding to the template (typically named `__lwcTmpl`). templateFn(stmt: EsStatement, optionalDedupeKey?: unknown) { if (optionalDedupeKey) { if (previouslyHoistedStatementKeysTmpl.has(optionalDedupeKey)) { @@ -66,6 +70,10 @@ export function createNewContext(templateOptions: TemplateOpts): { const shadowSlotToFnName = new Map(); let fnNameUniqueId = 0; + // At present, we only track shadow-slotted content. This is because the functions + // corresponding to shadow-slotted content are deduped and hoisted to the top of + // the template function, whereas light-dom-slotted content is inlined. It may be + // desirable to also track light-dom-slotted content at some future point in time. const slots = { shadow: { isDuplicate(uniqueNodeId: string) { diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index 0500f8c820..513e3939c2 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -126,7 +126,14 @@ export default function compileTemplate( } let tmplDecl = bExportTemplate( - optimizeAdjacentYieldStmts([...cxt.hoistedStatements.templateFn, ...statements]) + optimizeAdjacentYieldStmts([ + // Deep in the compiler, we may choose to hoist statements and declarations + // to the top of the template function. After `templateIrToEsTree`, these + // hoisted statements/declarations are prepended to the template function's + // body. + ...cxt.hoistedStatements.templateFn, + ...statements, + ]) ); // Ideally, we'd just do ${LWC_VERSION_COMMENT} in the code template, // but placeholders have a special meaning for `esTemplate`. @@ -139,7 +146,18 @@ export default function compileTemplate( ]; }); - let program = b.program([...getImports(), ...cxt.hoistedStatements.module, tmplDecl], 'module'); + let program = b.program( + [ + // All import declarations come first... + ...getImports(), + // ... followed by any statements or declarations that need to be hoisted + // to the top of the module scope... + ...cxt.hoistedStatements.module, + // ... followed by the template function declaration itself. + tmplDecl, + ], + 'module' + ); addScopeTokenDeclarations(program, filename, options.namespace, options.name); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index e050e7b016..99421e387b 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -39,6 +39,10 @@ import type { } from '@lwc/template-compiler'; import type { TransformerContext } from '../../types'; +// This function will be defined once and hoisted to the top of the template function. It'll be +// referenced deeper in the call stack where the function is called or passed as a parameter. +// It is a higher-order function that curries local variables that may be referenced by the +// shadow slot content. 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 @@ -47,6 +51,9 @@ const bGenerateShadowSlottedContent = esTemplateWithYield` ${/* shadow slot content */ is.statement} }; `; +// By passing in the set of local variables (which correspond 1:1 to the variables expected by +// the referenced function), `shadowSlottedContent` will be curried function that can generate +// shadow-slotted content. const bGenerateShadowSlottedContentRef = esTemplateWithYield` const shadowSlottedContent = ${/* reference to hoisted fn */ is.identifier}(${/* local vars */ is.identifier}); `; @@ -54,10 +61,10 @@ const bNullishGenerateShadowSlottedContent = esTemplateWithYield` const shadowSlottedContent = null; `; -const bContentMap = esTemplateWithYield` +const blightSlottedContentMap = esTemplateWithYield` const ${/* name of the content map */ is.identifier} = Object.create(null); `; -const bNullishContentMap = esTemplateWithYield` +const bNullishLightSlottedContentMap = esTemplateWithYield` const ${/* name of the content map */ is.identifier} = null; `; @@ -298,11 +305,11 @@ export function getSlottedContent( ) : bNullishGenerateShadowSlottedContent(); const lightSlottedContentMap = hasLightSlottedContent - ? bContentMap(b.identifier('lightSlottedContentMap')) - : bNullishContentMap(b.identifier('lightSlottedContentMap')); + ? blightSlottedContentMap(b.identifier('lightSlottedContentMap')) + : bNullishLightSlottedContentMap(b.identifier('lightSlottedContentMap')); const scopedSlottedContentMap = hasScopedSlottedContent - ? bContentMap(b.identifier('scopedSlottedContentMap')) - : bNullishContentMap(b.identifier('scopedSlottedContentMap')); + ? blightSlottedContentMap(b.identifier('scopedSlottedContentMap')) + : bNullishLightSlottedContentMap(b.identifier('scopedSlottedContentMap')); return bGenerateSlottedContent( shadowSlottedContentFn, From 158562d63afc77f65d5db241d9d45acc1dca7ea6 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 23:32:22 -0700 Subject: [PATCH 12/15] chore: add files that will be immediately deleted --- test.html.bad.js | 12733 ++++++++++++++++++++++++++++++++++++++++++++ test.html.good.js | 1012 ++++ 2 files changed, 13745 insertions(+) create mode 100644 test.html.bad.js create mode 100644 test.html.good.js diff --git a/test.html.bad.js b/test.html.bad.js new file mode 100644 index 0000000000..63e4b17fd3 --- /dev/null +++ b/test.html.bad.js @@ -0,0 +1,12733 @@ +const stylesheetScopeToken = "lwc-24o8hkv25p3"; +const hasScopedStylesheets = defaultScopedStylesheets !== undefined && defaultScopedStylesheets.length > 0; +import ChildComponentCtor_xAncientOne from "x/ancientOne"; +import {SYMBOL__GENERATE_MARKUP as __SYMBOL__GENERATE_MARKUP, fallbackTmplNoYield as __fallbackTmpl, renderStylesheets, hasScopedStaticStylesheets} from "@lwc/ssr-runtime"; +import ChildComponentCtor_xGreatGrandparent from "x/greatGrandparent"; +import ChildComponentCtor_xGrandparent from "x/grandparent"; +import ChildComponentCtor_xParent from "x/parent"; +import ChildComponentCtor_xChild from "x/child"; +import ChildComponentCtor_xGrandchild from "x/grandchild"; +import ChildComponentCtor_xZygote from "x/zygote"; +import ChildComponentCtor_xNascent from "x/nascent"; +import defaultStylesheets from "./test.css"; +import defaultScopedStylesheets from "./test.scoped.css?scoped=true"; +export default function __lwcTmpl($$emit, shadowSlottedContent, lightSlottedContent, scopedSlottedContent, Cmp, instance) { + let textContentBuffer = ''; + let didBufferTextContent = false; + const slotAttributeValue = null; + const contextfulParent = instance; + const isLightDom = Cmp.renderMode === 'light'; + if (!isLightDom) { + $$emit(`'); + if (shadowSlottedContent) { + shadowSlottedContent($$emit, contextfulParent); + } + } + /*LWC compiler v8.19.1*/ +} +__lwcTmpl.hasScopedStylesheets = hasScopedStylesheets; +__lwcTmpl.stylesheetScopeToken = stylesheetScopeToken; diff --git a/test.html.good.js b/test.html.good.js new file mode 100644 index 0000000000..89580a7262 --- /dev/null +++ b/test.html.good.js @@ -0,0 +1,1012 @@ +const stylesheetScopeToken = "lwc-24o8hkv25p3"; +const hasScopedStylesheets = defaultScopedStylesheets !== undefined && defaultScopedStylesheets.length > 0; +import ChildComponentCtor_xAncientOne from "x/ancientOne"; +import {SYMBOL__GENERATE_MARKUP as __SYMBOL__GENERATE_MARKUP, fallbackTmplNoYield as __fallbackTmpl, addSlottedContent, renderStylesheets, hasScopedStaticStylesheets} from "@lwc/ssr-runtime"; +import ChildComponentCtor_xGreatGrandparent from "x/greatGrandparent"; +import ChildComponentCtor_xGrandparent from "x/grandparent"; +import ChildComponentCtor_xParent from "x/parent"; +import ChildComponentCtor_xChild from "x/child"; +import ChildComponentCtor_xGrandchild from "x/grandchild"; +import ChildComponentCtor_xZygote from "x/zygote"; +import ChildComponentCtor_xNascent from "x/nascent"; +import defaultStylesheets from "./test.css"; +import defaultScopedStylesheets from "./test.scoped.css?scoped=true"; +export default function __lwcTmpl($$emit, shadowSlottedContent, lightSlottedContent, scopedSlottedContent, Cmp, instance) { + let textContentBuffer = ''; + let didBufferTextContent = false; + const slotAttributeValue = null; + const contextfulParent = instance; + const isLightDom = Cmp.renderMode === 'light'; + if (!isLightDom) { + $$emit(`'); + if (shadowSlottedContent) { + shadowSlottedContent($$emit, contextfulParent); + } + } + /*LWC compiler v8.19.1*/ +} +__lwcTmpl.hasScopedStylesheets = hasScopedStylesheets; +__lwcTmpl.stylesheetScopeToken = stylesheetScopeToken; From 01b6c297c9c5a794e444897b3e5ec435b241f945 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 27 May 2025 23:33:02 -0700 Subject: [PATCH 13/15] chore: delete files immediately --- test.html.bad.js | 12733 -------------------------------------------- test.html.good.js | 1012 ---- 2 files changed, 13745 deletions(-) delete mode 100644 test.html.bad.js delete mode 100644 test.html.good.js diff --git a/test.html.bad.js b/test.html.bad.js deleted file mode 100644 index 63e4b17fd3..0000000000 --- a/test.html.bad.js +++ /dev/null @@ -1,12733 +0,0 @@ -const stylesheetScopeToken = "lwc-24o8hkv25p3"; -const hasScopedStylesheets = defaultScopedStylesheets !== undefined && defaultScopedStylesheets.length > 0; -import ChildComponentCtor_xAncientOne from "x/ancientOne"; -import {SYMBOL__GENERATE_MARKUP as __SYMBOL__GENERATE_MARKUP, fallbackTmplNoYield as __fallbackTmpl, renderStylesheets, hasScopedStaticStylesheets} from "@lwc/ssr-runtime"; -import ChildComponentCtor_xGreatGrandparent from "x/greatGrandparent"; -import ChildComponentCtor_xGrandparent from "x/grandparent"; -import ChildComponentCtor_xParent from "x/parent"; -import ChildComponentCtor_xChild from "x/child"; -import ChildComponentCtor_xGrandchild from "x/grandchild"; -import ChildComponentCtor_xZygote from "x/zygote"; -import ChildComponentCtor_xNascent from "x/nascent"; -import defaultStylesheets from "./test.css"; -import defaultScopedStylesheets from "./test.scoped.css?scoped=true"; -export default function __lwcTmpl($$emit, shadowSlottedContent, lightSlottedContent, scopedSlottedContent, Cmp, instance) { - let textContentBuffer = ''; - let didBufferTextContent = false; - const slotAttributeValue = null; - const contextfulParent = instance; - const isLightDom = Cmp.renderMode === 'light'; - if (!isLightDom) { - $$emit(`'); - if (shadowSlottedContent) { - shadowSlottedContent($$emit, contextfulParent); - } - } - /*LWC compiler v8.19.1*/ -} -__lwcTmpl.hasScopedStylesheets = hasScopedStylesheets; -__lwcTmpl.stylesheetScopeToken = stylesheetScopeToken; diff --git a/test.html.good.js b/test.html.good.js deleted file mode 100644 index 89580a7262..0000000000 --- a/test.html.good.js +++ /dev/null @@ -1,1012 +0,0 @@ -const stylesheetScopeToken = "lwc-24o8hkv25p3"; -const hasScopedStylesheets = defaultScopedStylesheets !== undefined && defaultScopedStylesheets.length > 0; -import ChildComponentCtor_xAncientOne from "x/ancientOne"; -import {SYMBOL__GENERATE_MARKUP as __SYMBOL__GENERATE_MARKUP, fallbackTmplNoYield as __fallbackTmpl, addSlottedContent, renderStylesheets, hasScopedStaticStylesheets} from "@lwc/ssr-runtime"; -import ChildComponentCtor_xGreatGrandparent from "x/greatGrandparent"; -import ChildComponentCtor_xGrandparent from "x/grandparent"; -import ChildComponentCtor_xParent from "x/parent"; -import ChildComponentCtor_xChild from "x/child"; -import ChildComponentCtor_xGrandchild from "x/grandchild"; -import ChildComponentCtor_xZygote from "x/zygote"; -import ChildComponentCtor_xNascent from "x/nascent"; -import defaultStylesheets from "./test.css"; -import defaultScopedStylesheets from "./test.scoped.css?scoped=true"; -export default function __lwcTmpl($$emit, shadowSlottedContent, lightSlottedContent, scopedSlottedContent, Cmp, instance) { - let textContentBuffer = ''; - let didBufferTextContent = false; - const slotAttributeValue = null; - const contextfulParent = instance; - const isLightDom = Cmp.renderMode === 'light'; - if (!isLightDom) { - $$emit(`'); - if (shadowSlottedContent) { - shadowSlottedContent($$emit, contextfulParent); - } - } - /*LWC compiler v8.19.1*/ -} -__lwcTmpl.hasScopedStylesheets = hasScopedStylesheets; -__lwcTmpl.stylesheetScopeToken = stylesheetScopeToken; From 204c25f12651e9dbc77f94d191eb80a29ff16b27 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Mon, 2 Jun 2025 12:45:30 -0700 Subject: [PATCH 14/15] chore: readability improvement Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- packages/@lwc/ssr-compiler/src/compile-template/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index 4425d5c4d3..d2b0f356be 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -33,7 +33,7 @@ export function createNewContext(templateOptions: TemplateOpts): { } return false; }; - const getLocalVars = () => localVarStack.flatMap((stackFrameVars) => [...stackFrameVars]); + const getLocalVars = () => localVarStack.flatMap((varsSet) => Array.from(varsSet)); const hoistedStatements = { module: [] as EsStatement[], From af2815f7e08d81b5ecd2991bb76fc5a8c0f27c68 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Mon, 2 Jun 2025 12:47:50 -0700 Subject: [PATCH 15/15] fix: excessive verbosity Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- .../ssr-compiler/src/compile-template/context.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/compile-template/context.ts b/packages/@lwc/ssr-compiler/src/compile-template/context.ts index d2b0f356be..a2208ba6e2 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/context.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/context.ts @@ -39,18 +39,18 @@ export function createNewContext(templateOptions: TemplateOpts): { module: [] as EsStatement[], templateFn: [] as EsStatement[], }; - const previouslyHoistedStatementKeysMod = new Set(); - const previouslyHoistedStatementKeysTmpl = new Set(); + const hoistedModuleDedupe = new Set(); + const hoistedTemplateDedupe = new Set(); const hoist = { // Anything added here will be inserted at the top of the compiled template's // JS module. module(stmt: EsStatement, optionalDedupeKey?: unknown) { if (optionalDedupeKey) { - if (previouslyHoistedStatementKeysMod.has(optionalDedupeKey)) { + if (hoistedModuleDedupe.has(optionalDedupeKey)) { return; } - previouslyHoistedStatementKeysMod.add(optionalDedupeKey); + hoistedModuleDedupe.add(optionalDedupeKey); } hoistedStatements.module.push(stmt); }, @@ -58,10 +58,10 @@ export function createNewContext(templateOptions: TemplateOpts): { // corresponding to the template (typically named `__lwcTmpl`). templateFn(stmt: EsStatement, optionalDedupeKey?: unknown) { if (optionalDedupeKey) { - if (previouslyHoistedStatementKeysTmpl.has(optionalDedupeKey)) { + if (hoistedTemplateDedupe.has(optionalDedupeKey)) { return; } - previouslyHoistedStatementKeysTmpl.add(optionalDedupeKey); + hoistedTemplateDedupe.add(optionalDedupeKey); } hoistedStatements.templateFn.push(stmt); },