77
88import { produce } from 'immer' ;
99import { builders as b , is } from 'estree-toolkit' ;
10+ import { kebabCaseToCamelCase } from '@lwc/shared' ;
1011import { bAttributeValue , optimizeAdjacentYieldStmts } from '../../shared' ;
1112import { esTemplate , esTemplateWithYield } from '../../../estemplate' ;
1213import { irChildrenToEs , irToEs } from '../../ir-to-es' ;
1314import { isLiteral } from '../../shared' ;
1415import { expressionIrToEs } from '../../expression' ;
1516import { isNullableOf } from '../../../estree/validators' ;
1617import { isLastConcatenatedNode } from '../../adjacent-text-nodes' ;
17- import type { CallExpression as EsCallExpression , Expression as EsExpression } from 'estree' ;
1818
1919import type {
20+ CallExpression as EsCallExpression ,
21+ Expression as EsExpression ,
2022 Statement as EsStatement ,
2123 ExpressionStatement as EsExpressionStatement ,
24+ VariableDeclaration as EsVariableDeclaration ,
2225} from 'estree' ;
2326import type {
2427 ChildNode as IrChildNode ,
@@ -36,51 +39,58 @@ import type {
3639} from '@lwc/template-compiler' ;
3740import type { TransformerContext } from '../../types' ;
3841
39- const bGenerateSlottedContent = esTemplateWithYield `
40- const shadowSlottedContent = ${ /* hasShadowSlottedContent */ is . literal }
41- ? async function* __lwcGenerateSlottedContent(contextfulParent) {
42- // The 'contextfulParent' variable is shadowed here so that a contextful relationship
43- // is established between components rendered in slotted content & the "parent"
44- // component that contains the <slot>.
45-
46- ${ /* shadow slot content */ is . statement }
47- }
48- // Avoid creating the object unnecessarily
49- : null;
50-
51- const lightSlottedContentMap = ${ /* hasLightSlottedContent */ is . literal }
52- ? Object.create(null)
53- // Avoid creating the object unnecessarily
54- : null;
55-
56- // The containing slot treats scoped slotted content differently.
57- const scopedSlottedContentMap = ${ /* hasScopedSlottedContent */ is . literal }
58- ? Object.create(null)
59- // Avoid creating the object unnecessarily
60- : null;
61-
62- function addSlottedContent(name, fn, contentMap) {
63- let contentList = contentMap[name];
64- if (contentList) {
65- contentList.push(fn);
66- } else {
67- contentMap[name] = [fn];
68- }
69- }
42+ // This function will be defined once and hoisted to the top of the template function. It'll be
43+ // referenced deeper in the call stack where the function is called or passed as a parameter.
44+ // It is a higher-order function that curries local variables that may be referenced by the
45+ // shadow slot content.
46+ const bGenerateShadowSlottedContent = esTemplateWithYield `
47+ const ${ /* function name */ is . identifier } = (${ /* local vars */ is . identifier } ) => async function* ${ /* function name */ 0 } (contextfulParent) {
48+ // The 'contextfulParent' variable is shadowed here so that a contextful relationship
49+ // is established between components rendered in slotted content & the "parent"
50+ // component that contains the <slot>.
51+ ${ /* shadow slot content */ is . statement }
52+ };
53+ ` < EsVariableDeclaration > ;
54+ // By passing in the set of local variables (which correspond 1:1 to the variables expected by
55+ // the referenced function), `shadowSlottedContent` will be curried function that can generate
56+ // shadow-slotted content.
57+ const bGenerateShadowSlottedContentRef = esTemplateWithYield `
58+ const shadowSlottedContent = ${ /* reference to hoisted fn */ is . identifier } (${ /* local vars */ is . identifier } );
59+ ` < EsVariableDeclaration > ;
60+ const bNullishGenerateShadowSlottedContent = esTemplateWithYield `
61+ const shadowSlottedContent = null;
62+ ` < EsVariableDeclaration > ;
7063
71- ${ /* light DOM addLightContent statements */ is . expressionStatement }
72- ${ /* scoped slot addLightContent statements */ is . expressionStatement }
64+ const blightSlottedContentMap = esTemplateWithYield `
65+ const ${ /* name of the content map */ is . identifier } = Object.create(null);
66+ ` < EsVariableDeclaration > ;
67+ const bNullishLightSlottedContentMap = esTemplateWithYield `
68+ const ${ /* name of the content map */ is . identifier } = null;
69+ ` < EsVariableDeclaration > ;
70+
71+ const bGenerateSlottedContent = esTemplateWithYield `
72+ ${ /* const shadowSlottedContent = ... */ is . variableDeclaration }
73+ ${ /* const lightSlottedContentMap */ is . variableDeclaration }
74+ ${ /* const scopedSlottedContentMap */ is . variableDeclaration }
75+ ${ /* light DOM addLightContent statements */ is . expressionStatement }
76+ ${ /* scoped slot addLightContent statements */ is . expressionStatement }
7377` < EsStatement [ ] > ;
7478
7579// Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though
7680// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
7781// than a function _declaration_, so it isn't available to be referenced anywhere.
7882const bAddSlottedContent = esTemplate `
79- addSlottedContent(${ /* slot name */ is . expression } ?? "", async function* __lwcGenerateSlottedContent(contextfulParent, ${
80- /* scoped slot data variable */ isNullableOf ( is . identifier )
81- } , slotAttributeValue) {
82- ${ /* slot content */ is . statement }
83- }, ${ /* content map */ is . identifier } );
83+ addSlottedContent(
84+ ${ /* slot name */ is . expression } ?? "",
85+ async function* __lwcGenerateSlottedContent(
86+ contextfulParent,
87+ ${ /* scoped slot data variable */ isNullableOf ( is . identifier ) } ,
88+ slotAttributeValue)
89+ {
90+ ${ /* slot content */ is . statement }
91+ },
92+ ${ /* content map */ is . identifier }
93+ );
8494` < EsCallExpression > ;
8595
8696function getShadowSlottedContent ( slottableChildren : IrChildNode [ ] , cxt : TransformerContext ) {
@@ -155,7 +165,7 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex
155165 } ) ;
156166 const { isSlotted : originalIsSlotted } = cxt ;
157167 cxt . isSlotted = ancestorIndices . length > 1 || clone . type === 'Slot' ;
158- const slotContent = irToEs ( clone , cxt ) ;
168+ const slotContent = optimizeAdjacentYieldStmts ( irToEs ( clone , cxt ) ) ;
159169 cxt . isSlotted = originalIsSlotted ;
160170 results . push (
161171 b . expressionStatement (
@@ -246,24 +256,65 @@ export function getSlottedContent(
246256 bAddSlottedContent (
247257 slotName ,
248258 boundVariable ,
249- irChildrenToEs ( child . children , cxt ) ,
259+ optimizeAdjacentYieldStmts ( irChildrenToEs ( child . children , cxt ) ) ,
250260 b . identifier ( 'scopedSlottedContentMap' )
251261 )
252262 ) ;
253263 cxt . popLocalVars ( ) ;
254264 return addLightContentExpr ;
255265 } ) ;
256266
257- const hasShadowSlottedContent = b . literal ( shadowSlotContent . length > 0 ) ;
258- const hasLightSlottedContent = b . literal ( lightSlotContent . length > 0 ) ;
259- const hasScopedSlottedContent = b . literal ( scopedSlotContent . length > 0 ) ;
267+ const hasShadowSlottedContent = shadowSlotContent . length > 0 ;
268+ const hasLightSlottedContent = lightSlotContent . length > 0 ;
269+ const hasScopedSlottedContent = scopedSlotContent . length > 0 ;
260270 cxt . isSlotted = isSlotted ;
261271
272+ if ( hasShadowSlottedContent || hasLightSlottedContent || hasScopedSlottedContent ) {
273+ cxt . import ( 'addSlottedContent' ) ;
274+ }
275+
276+ // Elsewhere, nodes and their subtrees are cloned. This design decision means that
277+ // the node objects themselves cannot be used as unique identifiers (e.g. as keys
278+ // in a map). However, for a given template, a node's location information does
279+ // uniquely identify that node.
280+ const uniqueNodeId = `${ node . name } :${ node . location . start } :${ node . location . end } ` ;
281+
282+ const localVars = cxt . getLocalVars ( ) ;
283+ const localVarIds = localVars . map ( b . identifier ) ;
284+
285+ if ( hasShadowSlottedContent && ! cxt . slots . shadow . isDuplicate ( uniqueNodeId ) ) {
286+ // Colon characters in <lwc:component> element name will result in an invalid
287+ // JavaScript identifier if not otherwise accounted for.
288+ const kebabCmpName = kebabCaseToCamelCase ( node . name ) . replace ( ':' , '_' ) ;
289+ const shadowSlotContentFnName = cxt . slots . shadow . register ( uniqueNodeId , kebabCmpName ) ;
290+ const shadowSlottedContentFn = bGenerateShadowSlottedContent (
291+ b . identifier ( shadowSlotContentFnName ) ,
292+ // If the slot-fn were defined here instead of hoisted to the top of the module,
293+ // the local variables (e.g. from for:each) would be closed-over. When hoisted,
294+ // however, we need to curry these variables.
295+ localVarIds ,
296+ shadowSlotContent
297+ ) ;
298+ cxt . hoist . templateFn ( shadowSlottedContentFn , node ) ;
299+ }
300+
301+ const shadowSlottedContentFn = hasShadowSlottedContent
302+ ? bGenerateShadowSlottedContentRef (
303+ b . identifier ( cxt . slots . shadow . getFnName ( uniqueNodeId ) ! ) ,
304+ localVarIds
305+ )
306+ : bNullishGenerateShadowSlottedContent ( ) ;
307+ const lightSlottedContentMap = hasLightSlottedContent
308+ ? blightSlottedContentMap ( b . identifier ( 'lightSlottedContentMap' ) )
309+ : bNullishLightSlottedContentMap ( b . identifier ( 'lightSlottedContentMap' ) ) ;
310+ const scopedSlottedContentMap = hasScopedSlottedContent
311+ ? blightSlottedContentMap ( b . identifier ( 'scopedSlottedContentMap' ) )
312+ : bNullishLightSlottedContentMap ( b . identifier ( 'scopedSlottedContentMap' ) ) ;
313+
262314 return bGenerateSlottedContent (
263- hasShadowSlottedContent ,
264- shadowSlotContent ,
265- hasLightSlottedContent ,
266- hasScopedSlottedContent ,
315+ shadowSlottedContentFn ,
316+ lightSlottedContentMap ,
317+ scopedSlottedContentMap ,
267318 lightSlotContent ,
268319 scopedSlotContent
269320 ) ;
0 commit comments