@lwc/ssr-compiler 8.19.0 → 8.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,5 +7,6 @@ export declare function templateIrToEsTree(node: IrNode, contextOpts: TemplateOp
7
7
  addImport: (imports: string | string[] | Record<string, string | undefined>, source?: string) => void;
8
8
  getImports: () => import("estree").ImportDeclaration[];
9
9
  statements: EsStatement[];
10
+ cxt: TransformerContext;
10
11
  };
11
12
  //# sourceMappingURL=ir-to-es.d.ts.map
@@ -1,14 +1,31 @@
1
1
  import type { Node as IrNode } from '@lwc/template-compiler';
2
2
  import type { Statement as EsStatement } from 'estree';
3
3
  export type Transformer<T extends IrNode = IrNode> = (node: T, cxt: TransformerContext) => EsStatement[];
4
+ export interface SlotMetadataContext {
5
+ shadow: {
6
+ isDuplicate: (uniqueNodeId: string) => boolean;
7
+ register: (uniqueNodeId: string, kebabCmpName: string) => string;
8
+ getFnName: (uniqueNodeId: string) => string | null;
9
+ };
10
+ }
4
11
  export interface TransformerContext {
5
12
  pushLocalVars: (vars: string[]) => void;
6
13
  popLocalVars: () => void;
7
14
  isLocalVar: (varName: string | null | undefined) => boolean;
15
+ getLocalVars: () => string[];
8
16
  templateOptions: TemplateOpts;
9
17
  siblings: IrNode[] | undefined;
10
18
  currentNodeIndex: number | undefined;
11
19
  isSlotted?: boolean;
20
+ hoistedStatements: {
21
+ module: EsStatement[];
22
+ templateFn: EsStatement[];
23
+ };
24
+ hoist: {
25
+ module: (stmt: EsStatement, optionalDedupeKey?: unknown) => void;
26
+ templateFn: (stmt: EsStatement, optionalDedupeKey?: unknown) => void;
27
+ };
28
+ slots: SlotMetadataContext;
12
29
  import: (imports: string | string[] | Record<string, string | undefined>, source?: string) => void;
13
30
  }
14
31
  export interface TemplateOpts {
package/dist/index.cjs.js CHANGED
@@ -27,7 +27,7 @@ var util = require('util');
27
27
  const EMIT_IDENT = estreeToolkit.builders.identifier('$$emit');
28
28
  /** Function names that may be transmogrified. All should start with `__lwc`. */
29
29
  // Rollup may rename variables to prevent shadowing. When it does, it uses the format `foo$0`, `foo$1`, etc.
30
- const TRANSMOGRIFY_TARGET = /^__lwc(GenerateMarkup|GenerateSlottedContent|Tmpl)(?:\$\d+)?$/;
30
+ const TRANSMOGRIFY_TARGET = /^__lwc(Generate|Tmpl).*$/;
31
31
  const isWithinFn = (nodePath) => {
32
32
  const { node } = nodePath;
33
33
  if (!node) {
@@ -1569,49 +1569,55 @@ if (process.env.NODE_ENV !== 'production') {
1569
1569
  * SPDX-License-Identifier: MIT
1570
1570
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
1571
1571
  */
1572
+ // This function will be defined once and hoisted to the top of the template function. It'll be
1573
+ // referenced deeper in the call stack where the function is called or passed as a parameter.
1574
+ // It is a higher-order function that curries local variables that may be referenced by the
1575
+ // shadow slot content.
1576
+ const bGenerateShadowSlottedContent = (esTemplateWithYield `
1577
+ const ${ /* function name */estreeToolkit.is.identifier} = (${ /* local vars */estreeToolkit.is.identifier}) => async function* ${ /* function name */0}(contextfulParent) {
1578
+ // The 'contextfulParent' variable is shadowed here so that a contextful relationship
1579
+ // is established between components rendered in slotted content & the "parent"
1580
+ // component that contains the <slot>.
1581
+ ${ /* shadow slot content */estreeToolkit.is.statement}
1582
+ };
1583
+ `);
1584
+ // By passing in the set of local variables (which correspond 1:1 to the variables expected by
1585
+ // the referenced function), `shadowSlottedContent` will be curried function that can generate
1586
+ // shadow-slotted content.
1587
+ const bGenerateShadowSlottedContentRef = (esTemplateWithYield `
1588
+ const shadowSlottedContent = ${ /* reference to hoisted fn */estreeToolkit.is.identifier}(${ /* local vars */estreeToolkit.is.identifier});
1589
+ `);
1590
+ const bNullishGenerateShadowSlottedContent = (esTemplateWithYield `
1591
+ const shadowSlottedContent = null;
1592
+ `);
1593
+ const blightSlottedContentMap = (esTemplateWithYield `
1594
+ const ${ /* name of the content map */estreeToolkit.is.identifier} = Object.create(null);
1595
+ `);
1596
+ const bNullishLightSlottedContentMap = (esTemplateWithYield `
1597
+ const ${ /* name of the content map */estreeToolkit.is.identifier} = null;
1598
+ `);
1572
1599
  const bGenerateSlottedContent = (esTemplateWithYield `
1573
- const shadowSlottedContent = ${ /* hasShadowSlottedContent */estreeToolkit.is.literal}
1574
- ? async function* __lwcGenerateSlottedContent(contextfulParent) {
1575
- // The 'contextfulParent' variable is shadowed here so that a contextful relationship
1576
- // is established between components rendered in slotted content & the "parent"
1577
- // component that contains the <slot>.
1578
-
1579
- ${ /* shadow slot content */estreeToolkit.is.statement}
1580
- }
1581
- // Avoid creating the object unnecessarily
1582
- : null;
1583
-
1584
- const lightSlottedContentMap = ${ /* hasLightSlottedContent */estreeToolkit.is.literal}
1585
- ? Object.create(null)
1586
- // Avoid creating the object unnecessarily
1587
- : null;
1588
-
1589
- // The containing slot treats scoped slotted content differently.
1590
- const scopedSlottedContentMap = ${ /* hasScopedSlottedContent */estreeToolkit.is.literal}
1591
- ? Object.create(null)
1592
- // Avoid creating the object unnecessarily
1593
- : null;
1594
-
1595
- function addSlottedContent(name, fn, contentMap) {
1596
- let contentList = contentMap[name];
1597
- if (contentList) {
1598
- contentList.push(fn);
1599
- } else {
1600
- contentMap[name] = [fn];
1601
- }
1602
- }
1603
-
1604
- ${ /* light DOM addLightContent statements */estreeToolkit.is.expressionStatement}
1605
- ${ /* scoped slot addLightContent statements */estreeToolkit.is.expressionStatement}
1600
+ ${ /* const shadowSlottedContent = ... */estreeToolkit.is.variableDeclaration}
1601
+ ${ /* const lightSlottedContentMap */estreeToolkit.is.variableDeclaration}
1602
+ ${ /* const scopedSlottedContentMap */estreeToolkit.is.variableDeclaration}
1603
+ ${ /* light DOM addLightContent statements */estreeToolkit.is.expressionStatement}
1604
+ ${ /* scoped slot addLightContent statements */estreeToolkit.is.expressionStatement}
1606
1605
  `);
1607
1606
  // Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though
1608
1607
  // it may be repeated multiple times in the same scope, because it's a function _expression_ rather
1609
1608
  // than a function _declaration_, so it isn't available to be referenced anywhere.
1610
1609
  const bAddSlottedContent = (esTemplate `
1611
- addSlottedContent(${ /* slot name */estreeToolkit.is.expression} ?? "", async function* __lwcGenerateSlottedContent(contextfulParent, ${
1612
- /* scoped slot data variable */ isNullableOf(estreeToolkit.is.identifier)}, slotAttributeValue) {
1613
- ${ /* slot content */estreeToolkit.is.statement}
1614
- }, ${ /* content map */estreeToolkit.is.identifier});
1610
+ addSlottedContent(
1611
+ ${ /* slot name */estreeToolkit.is.expression} ?? "",
1612
+ async function* __lwcGenerateSlottedContent(
1613
+ contextfulParent,
1614
+ ${ /* scoped slot data variable */isNullableOf(estreeToolkit.is.identifier)},
1615
+ slotAttributeValue)
1616
+ {
1617
+ ${ /* slot content */estreeToolkit.is.statement}
1618
+ },
1619
+ ${ /* content map */estreeToolkit.is.identifier}
1620
+ );
1615
1621
  `);
1616
1622
  function getShadowSlottedContent(slottableChildren, cxt) {
1617
1623
  return optimizeAdjacentYieldStmts(irChildrenToEs(slottableChildren, cxt, (child) => {
@@ -1675,7 +1681,7 @@ function getLightSlottedContent(rootNodes, cxt) {
1675
1681
  });
1676
1682
  const { isSlotted: originalIsSlotted } = cxt;
1677
1683
  cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
1678
- const slotContent = irToEs(clone, cxt);
1684
+ const slotContent = optimizeAdjacentYieldStmts(irToEs(clone, cxt));
1679
1685
  cxt.isSlotted = originalIsSlotted;
1680
1686
  results.push(estreeToolkit.builders.expressionStatement(bAddSlottedContent(slotName, null, slotContent, estreeToolkit.builders.identifier('lightSlottedContentMap'))));
1681
1687
  };
@@ -1735,15 +1741,46 @@ function getSlottedContent(node, cxt) {
1735
1741
  ? estreeToolkit.builders.literal(child.slotName.value)
1736
1742
  : expressionIrToEs(child.slotName, cxt);
1737
1743
  // TODO [#4768]: what if the bound variable is `generateMarkup` or some framework-specific identifier?
1738
- const addLightContentExpr = estreeToolkit.builders.expressionStatement(bAddSlottedContent(slotName, boundVariable, irChildrenToEs(child.children, cxt), estreeToolkit.builders.identifier('scopedSlottedContentMap')));
1744
+ const addLightContentExpr = estreeToolkit.builders.expressionStatement(bAddSlottedContent(slotName, boundVariable, optimizeAdjacentYieldStmts(irChildrenToEs(child.children, cxt)), estreeToolkit.builders.identifier('scopedSlottedContentMap')));
1739
1745
  cxt.popLocalVars();
1740
1746
  return addLightContentExpr;
1741
1747
  });
1742
- const hasShadowSlottedContent = estreeToolkit.builders.literal(shadowSlotContent.length > 0);
1743
- const hasLightSlottedContent = estreeToolkit.builders.literal(lightSlotContent.length > 0);
1744
- const hasScopedSlottedContent = estreeToolkit.builders.literal(scopedSlotContent.length > 0);
1748
+ const hasShadowSlottedContent = shadowSlotContent.length > 0;
1749
+ const hasLightSlottedContent = lightSlotContent.length > 0;
1750
+ const hasScopedSlottedContent = scopedSlotContent.length > 0;
1745
1751
  cxt.isSlotted = isSlotted;
1746
- return bGenerateSlottedContent(hasShadowSlottedContent, shadowSlotContent, hasLightSlottedContent, hasScopedSlottedContent, lightSlotContent, scopedSlotContent);
1752
+ if (hasShadowSlottedContent || hasLightSlottedContent || hasScopedSlottedContent) {
1753
+ cxt.import('addSlottedContent');
1754
+ }
1755
+ // Elsewhere, nodes and their subtrees are cloned. This design decision means that
1756
+ // the node objects themselves cannot be used as unique identifiers (e.g. as keys
1757
+ // in a map). However, for a given template, a node's location information does
1758
+ // uniquely identify that node.
1759
+ const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`;
1760
+ const localVars = cxt.getLocalVars();
1761
+ const localVarIds = localVars.map(estreeToolkit.builders.identifier);
1762
+ if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) {
1763
+ // Colon characters in <lwc:component> element name will result in an invalid
1764
+ // JavaScript identifier if not otherwise accounted for.
1765
+ const kebabCmpName = shared.kebabCaseToCamelCase(node.name).replace(':', '_');
1766
+ const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName);
1767
+ const shadowSlottedContentFn = bGenerateShadowSlottedContent(estreeToolkit.builders.identifier(shadowSlotContentFnName),
1768
+ // If the slot-fn were defined here instead of hoisted to the top of the module,
1769
+ // the local variables (e.g. from for:each) would be closed-over. When hoisted,
1770
+ // however, we need to curry these variables.
1771
+ localVarIds, shadowSlotContent);
1772
+ cxt.hoist.templateFn(shadowSlottedContentFn, node);
1773
+ }
1774
+ const shadowSlottedContentFn = hasShadowSlottedContent
1775
+ ? bGenerateShadowSlottedContentRef(estreeToolkit.builders.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)), localVarIds)
1776
+ : bNullishGenerateShadowSlottedContent();
1777
+ const lightSlottedContentMap = hasLightSlottedContent
1778
+ ? blightSlottedContentMap(estreeToolkit.builders.identifier('lightSlottedContentMap'))
1779
+ : bNullishLightSlottedContentMap(estreeToolkit.builders.identifier('lightSlottedContentMap'));
1780
+ const scopedSlottedContentMap = hasScopedSlottedContent
1781
+ ? blightSlottedContentMap(estreeToolkit.builders.identifier('scopedSlottedContentMap'))
1782
+ : bNullishLightSlottedContentMap(estreeToolkit.builders.identifier('scopedSlottedContentMap'));
1783
+ return bGenerateSlottedContent(shadowSlottedContentFn, lightSlottedContentMap, scopedSlottedContentMap, lightSlotContent, scopedSlotContent);
1747
1784
  }
1748
1785
 
1749
1786
  /*
@@ -2290,13 +2327,72 @@ function createNewContext(templateOptions) {
2290
2327
  }
2291
2328
  return false;
2292
2329
  };
2330
+ const getLocalVars = () => localVarStack.flatMap((varsSet) => Array.from(varsSet));
2331
+ const hoistedStatements = {
2332
+ module: [],
2333
+ templateFn: [],
2334
+ };
2335
+ const hoistedModuleDedupe = new Set();
2336
+ const hoistedTemplateDedupe = new Set();
2337
+ const hoist = {
2338
+ // Anything added here will be inserted at the top of the compiled template's
2339
+ // JS module.
2340
+ module(stmt, optionalDedupeKey) {
2341
+ if (optionalDedupeKey) {
2342
+ if (hoistedModuleDedupe.has(optionalDedupeKey)) {
2343
+ return;
2344
+ }
2345
+ hoistedModuleDedupe.add(optionalDedupeKey);
2346
+ }
2347
+ hoistedStatements.module.push(stmt);
2348
+ },
2349
+ // Anything added here will be inserted at the top of the JavaScript function
2350
+ // corresponding to the template (typically named `__lwcTmpl`).
2351
+ templateFn(stmt, optionalDedupeKey) {
2352
+ if (optionalDedupeKey) {
2353
+ if (hoistedTemplateDedupe.has(optionalDedupeKey)) {
2354
+ return;
2355
+ }
2356
+ hoistedTemplateDedupe.add(optionalDedupeKey);
2357
+ }
2358
+ hoistedStatements.templateFn.push(stmt);
2359
+ },
2360
+ };
2361
+ const shadowSlotToFnName = new Map();
2362
+ let fnNameUniqueId = 0;
2363
+ // At present, we only track shadow-slotted content. This is because the functions
2364
+ // corresponding to shadow-slotted content are deduped and hoisted to the top of
2365
+ // the template function, whereas light-dom-slotted content is inlined. It may be
2366
+ // desirable to also track light-dom-slotted content at some future point in time.
2367
+ const slots = {
2368
+ shadow: {
2369
+ isDuplicate(uniqueNodeId) {
2370
+ return shadowSlotToFnName.has(uniqueNodeId);
2371
+ },
2372
+ register(uniqueNodeId, kebabCmpName) {
2373
+ if (slots.shadow.isDuplicate(uniqueNodeId)) {
2374
+ return shadowSlotToFnName.get(uniqueNodeId);
2375
+ }
2376
+ const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`;
2377
+ shadowSlotToFnName.set(uniqueNodeId, shadowSlotContentFnName);
2378
+ return shadowSlotContentFnName;
2379
+ },
2380
+ getFnName(uniqueNodeId) {
2381
+ return shadowSlotToFnName.get(uniqueNodeId) ?? null;
2382
+ },
2383
+ },
2384
+ };
2293
2385
  return {
2294
2386
  getImports: () => importManager.getImportDeclarations(),
2295
2387
  cxt: {
2296
2388
  pushLocalVars,
2297
2389
  popLocalVars,
2298
2390
  isLocalVar,
2391
+ getLocalVars,
2299
2392
  templateOptions,
2393
+ hoist,
2394
+ hoistedStatements,
2395
+ slots,
2300
2396
  import: importManager.add.bind(importManager),
2301
2397
  siblings: undefined,
2302
2398
  currentNodeIndex: undefined,
@@ -2409,6 +2505,7 @@ function templateIrToEsTree(node, contextOpts) {
2409
2505
  addImport: cxt.import,
2410
2506
  getImports,
2411
2507
  statements,
2508
+ cxt,
2412
2509
  };
2413
2510
  }
2414
2511
 
@@ -2480,6 +2577,7 @@ function compileTemplate(src, filename, options, compilationMode) {
2480
2577
  experimentalComputedMemberExpression: options.experimentalComputedMemberExpression,
2481
2578
  experimentalComplexExpressions: options.experimentalComplexExpressions,
2482
2579
  enableDynamicComponents: options.enableDynamicComponents,
2580
+ enableLwcOn: options.enableLwcOn,
2483
2581
  preserveHtmlComments: options.preserveHtmlComments,
2484
2582
  enableStaticContentOptimization: options.enableStaticContentOptimization,
2485
2583
  instrumentation: options.instrumentation,
@@ -2503,7 +2601,7 @@ function compileTemplate(src, filename, options, compilationMode) {
2503
2601
  }
2504
2602
  const preserveComments = !!root.directives.find((directive) => directive.name === 'PreserveComments')?.value?.value;
2505
2603
  const experimentalComplexExpressions = Boolean(options.experimentalComplexExpressions);
2506
- const { addImport, getImports, statements } = templateIrToEsTree(root, {
2604
+ const { addImport, getImports, statements, cxt } = templateIrToEsTree(root, {
2507
2605
  preserveComments,
2508
2606
  experimentalComplexExpressions,
2509
2607
  });
@@ -2511,7 +2609,14 @@ function compileTemplate(src, filename, options, compilationMode) {
2511
2609
  for (const [imports, source] of getStylesheetImports(filename)) {
2512
2610
  addImport(imports, source);
2513
2611
  }
2514
- let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts(statements));
2612
+ let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts([
2613
+ // Deep in the compiler, we may choose to hoist statements and declarations
2614
+ // to the top of the template function. After `templateIrToEsTree`, these
2615
+ // hoisted statements/declarations are prepended to the template function's
2616
+ // body.
2617
+ ...cxt.hoistedStatements.templateFn,
2618
+ ...statements,
2619
+ ]));
2515
2620
  // Ideally, we'd just do ${LWC_VERSION_COMMENT} in the code template,
2516
2621
  // but placeholders have a special meaning for `esTemplate`.
2517
2622
  tmplDecl = immer.produce(tmplDecl, (draft) => {
@@ -2522,7 +2627,15 @@ function compileTemplate(src, filename, options, compilationMode) {
2522
2627
  },
2523
2628
  ];
2524
2629
  });
2525
- let program = estreeToolkit.builders.program([...getImports(), tmplDecl], 'module');
2630
+ let program = estreeToolkit.builders.program([
2631
+ // All import declarations come first...
2632
+ ...getImports(),
2633
+ // ... followed by any statements or declarations that need to be hoisted
2634
+ // to the top of the module scope...
2635
+ ...cxt.hoistedStatements.module,
2636
+ // ... followed by the template function declaration itself.
2637
+ tmplDecl,
2638
+ ], 'module');
2526
2639
  addScopeTokenDeclarations(program, filename, options.namespace, options.name);
2527
2640
  if (compilationMode === 'async' || compilationMode === 'sync') {
2528
2641
  program = transmogrify(program, compilationMode);
@@ -2553,5 +2666,5 @@ function compileTemplateForSSR(src, filename, options, mode = shared.DEFAULT_SSR
2553
2666
 
2554
2667
  exports.compileComponentForSSR = compileComponentForSSR;
2555
2668
  exports.compileTemplateForSSR = compileTemplateForSSR;
2556
- /** version: 8.19.0 */
2669
+ /** version: 8.20.0 */
2557
2670
  //# sourceMappingURL=index.cjs.js.map
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Copyright (c) 2025 Salesforce, Inc.
3
3
  */
4
- import { AMBIGUOUS_PROP_SET, DISALLOWED_PROP_SET, LWC_VERSION_COMMENT, normalizeStyleAttributeValue, normalizeTabIndex, StringReplace, StringTrim, entries, isUndefined, HTML_NAMESPACE, isVoidElement, isBooleanAttribute, DEFAULT_SSR_MODE, generateCustomElementTagName } from '@lwc/shared';
4
+ import { AMBIGUOUS_PROP_SET, DISALLOWED_PROP_SET, LWC_VERSION_COMMENT, normalizeStyleAttributeValue, normalizeTabIndex, StringReplace, StringTrim, entries, kebabCaseToCamelCase, isUndefined, HTML_NAMESPACE, isVoidElement, isBooleanAttribute, DEFAULT_SSR_MODE, generateCustomElementTagName } from '@lwc/shared';
5
5
  import { generate } from 'astring';
6
6
  import { builders, traverse, is } from 'estree-toolkit';
7
7
  import { parseModule } from 'meriyah';
@@ -23,7 +23,7 @@ import { inspect } from 'util';
23
23
  const EMIT_IDENT = builders.identifier('$$emit');
24
24
  /** Function names that may be transmogrified. All should start with `__lwc`. */
25
25
  // Rollup may rename variables to prevent shadowing. When it does, it uses the format `foo$0`, `foo$1`, etc.
26
- const TRANSMOGRIFY_TARGET = /^__lwc(GenerateMarkup|GenerateSlottedContent|Tmpl)(?:\$\d+)?$/;
26
+ const TRANSMOGRIFY_TARGET = /^__lwc(Generate|Tmpl).*$/;
27
27
  const isWithinFn = (nodePath) => {
28
28
  const { node } = nodePath;
29
29
  if (!node) {
@@ -1565,49 +1565,55 @@ if (process.env.NODE_ENV !== 'production') {
1565
1565
  * SPDX-License-Identifier: MIT
1566
1566
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
1567
1567
  */
1568
+ // This function will be defined once and hoisted to the top of the template function. It'll be
1569
+ // referenced deeper in the call stack where the function is called or passed as a parameter.
1570
+ // It is a higher-order function that curries local variables that may be referenced by the
1571
+ // shadow slot content.
1572
+ const bGenerateShadowSlottedContent = (esTemplateWithYield `
1573
+ const ${ /* function name */is.identifier} = (${ /* local vars */is.identifier}) => async function* ${ /* function name */0}(contextfulParent) {
1574
+ // The 'contextfulParent' variable is shadowed here so that a contextful relationship
1575
+ // is established between components rendered in slotted content & the "parent"
1576
+ // component that contains the <slot>.
1577
+ ${ /* shadow slot content */is.statement}
1578
+ };
1579
+ `);
1580
+ // By passing in the set of local variables (which correspond 1:1 to the variables expected by
1581
+ // the referenced function), `shadowSlottedContent` will be curried function that can generate
1582
+ // shadow-slotted content.
1583
+ const bGenerateShadowSlottedContentRef = (esTemplateWithYield `
1584
+ const shadowSlottedContent = ${ /* reference to hoisted fn */is.identifier}(${ /* local vars */is.identifier});
1585
+ `);
1586
+ const bNullishGenerateShadowSlottedContent = (esTemplateWithYield `
1587
+ const shadowSlottedContent = null;
1588
+ `);
1589
+ const blightSlottedContentMap = (esTemplateWithYield `
1590
+ const ${ /* name of the content map */is.identifier} = Object.create(null);
1591
+ `);
1592
+ const bNullishLightSlottedContentMap = (esTemplateWithYield `
1593
+ const ${ /* name of the content map */is.identifier} = null;
1594
+ `);
1568
1595
  const bGenerateSlottedContent = (esTemplateWithYield `
1569
- const shadowSlottedContent = ${ /* hasShadowSlottedContent */is.literal}
1570
- ? async function* __lwcGenerateSlottedContent(contextfulParent) {
1571
- // The 'contextfulParent' variable is shadowed here so that a contextful relationship
1572
- // is established between components rendered in slotted content & the "parent"
1573
- // component that contains the <slot>.
1574
-
1575
- ${ /* shadow slot content */is.statement}
1576
- }
1577
- // Avoid creating the object unnecessarily
1578
- : null;
1579
-
1580
- const lightSlottedContentMap = ${ /* hasLightSlottedContent */is.literal}
1581
- ? Object.create(null)
1582
- // Avoid creating the object unnecessarily
1583
- : null;
1584
-
1585
- // The containing slot treats scoped slotted content differently.
1586
- const scopedSlottedContentMap = ${ /* hasScopedSlottedContent */is.literal}
1587
- ? Object.create(null)
1588
- // Avoid creating the object unnecessarily
1589
- : null;
1590
-
1591
- function addSlottedContent(name, fn, contentMap) {
1592
- let contentList = contentMap[name];
1593
- if (contentList) {
1594
- contentList.push(fn);
1595
- } else {
1596
- contentMap[name] = [fn];
1597
- }
1598
- }
1599
-
1600
- ${ /* light DOM addLightContent statements */is.expressionStatement}
1601
- ${ /* scoped slot addLightContent statements */is.expressionStatement}
1596
+ ${ /* const shadowSlottedContent = ... */is.variableDeclaration}
1597
+ ${ /* const lightSlottedContentMap */is.variableDeclaration}
1598
+ ${ /* const scopedSlottedContentMap */is.variableDeclaration}
1599
+ ${ /* light DOM addLightContent statements */is.expressionStatement}
1600
+ ${ /* scoped slot addLightContent statements */is.expressionStatement}
1602
1601
  `);
1603
1602
  // Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though
1604
1603
  // it may be repeated multiple times in the same scope, because it's a function _expression_ rather
1605
1604
  // than a function _declaration_, so it isn't available to be referenced anywhere.
1606
1605
  const bAddSlottedContent = (esTemplate `
1607
- addSlottedContent(${ /* slot name */is.expression} ?? "", async function* __lwcGenerateSlottedContent(contextfulParent, ${
1608
- /* scoped slot data variable */ isNullableOf(is.identifier)}, slotAttributeValue) {
1609
- ${ /* slot content */is.statement}
1610
- }, ${ /* content map */is.identifier});
1606
+ addSlottedContent(
1607
+ ${ /* slot name */is.expression} ?? "",
1608
+ async function* __lwcGenerateSlottedContent(
1609
+ contextfulParent,
1610
+ ${ /* scoped slot data variable */isNullableOf(is.identifier)},
1611
+ slotAttributeValue)
1612
+ {
1613
+ ${ /* slot content */is.statement}
1614
+ },
1615
+ ${ /* content map */is.identifier}
1616
+ );
1611
1617
  `);
1612
1618
  function getShadowSlottedContent(slottableChildren, cxt) {
1613
1619
  return optimizeAdjacentYieldStmts(irChildrenToEs(slottableChildren, cxt, (child) => {
@@ -1671,7 +1677,7 @@ function getLightSlottedContent(rootNodes, cxt) {
1671
1677
  });
1672
1678
  const { isSlotted: originalIsSlotted } = cxt;
1673
1679
  cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
1674
- const slotContent = irToEs(clone, cxt);
1680
+ const slotContent = optimizeAdjacentYieldStmts(irToEs(clone, cxt));
1675
1681
  cxt.isSlotted = originalIsSlotted;
1676
1682
  results.push(builders.expressionStatement(bAddSlottedContent(slotName, null, slotContent, builders.identifier('lightSlottedContentMap'))));
1677
1683
  };
@@ -1731,15 +1737,46 @@ function getSlottedContent(node, cxt) {
1731
1737
  ? builders.literal(child.slotName.value)
1732
1738
  : expressionIrToEs(child.slotName, cxt);
1733
1739
  // TODO [#4768]: what if the bound variable is `generateMarkup` or some framework-specific identifier?
1734
- const addLightContentExpr = builders.expressionStatement(bAddSlottedContent(slotName, boundVariable, irChildrenToEs(child.children, cxt), builders.identifier('scopedSlottedContentMap')));
1740
+ const addLightContentExpr = builders.expressionStatement(bAddSlottedContent(slotName, boundVariable, optimizeAdjacentYieldStmts(irChildrenToEs(child.children, cxt)), builders.identifier('scopedSlottedContentMap')));
1735
1741
  cxt.popLocalVars();
1736
1742
  return addLightContentExpr;
1737
1743
  });
1738
- const hasShadowSlottedContent = builders.literal(shadowSlotContent.length > 0);
1739
- const hasLightSlottedContent = builders.literal(lightSlotContent.length > 0);
1740
- const hasScopedSlottedContent = builders.literal(scopedSlotContent.length > 0);
1744
+ const hasShadowSlottedContent = shadowSlotContent.length > 0;
1745
+ const hasLightSlottedContent = lightSlotContent.length > 0;
1746
+ const hasScopedSlottedContent = scopedSlotContent.length > 0;
1741
1747
  cxt.isSlotted = isSlotted;
1742
- return bGenerateSlottedContent(hasShadowSlottedContent, shadowSlotContent, hasLightSlottedContent, hasScopedSlottedContent, lightSlotContent, scopedSlotContent);
1748
+ if (hasShadowSlottedContent || hasLightSlottedContent || hasScopedSlottedContent) {
1749
+ cxt.import('addSlottedContent');
1750
+ }
1751
+ // Elsewhere, nodes and their subtrees are cloned. This design decision means that
1752
+ // the node objects themselves cannot be used as unique identifiers (e.g. as keys
1753
+ // in a map). However, for a given template, a node's location information does
1754
+ // uniquely identify that node.
1755
+ const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`;
1756
+ const localVars = cxt.getLocalVars();
1757
+ const localVarIds = localVars.map(builders.identifier);
1758
+ if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) {
1759
+ // Colon characters in <lwc:component> element name will result in an invalid
1760
+ // JavaScript identifier if not otherwise accounted for.
1761
+ const kebabCmpName = kebabCaseToCamelCase(node.name).replace(':', '_');
1762
+ const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName);
1763
+ const shadowSlottedContentFn = bGenerateShadowSlottedContent(builders.identifier(shadowSlotContentFnName),
1764
+ // If the slot-fn were defined here instead of hoisted to the top of the module,
1765
+ // the local variables (e.g. from for:each) would be closed-over. When hoisted,
1766
+ // however, we need to curry these variables.
1767
+ localVarIds, shadowSlotContent);
1768
+ cxt.hoist.templateFn(shadowSlottedContentFn, node);
1769
+ }
1770
+ const shadowSlottedContentFn = hasShadowSlottedContent
1771
+ ? bGenerateShadowSlottedContentRef(builders.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)), localVarIds)
1772
+ : bNullishGenerateShadowSlottedContent();
1773
+ const lightSlottedContentMap = hasLightSlottedContent
1774
+ ? blightSlottedContentMap(builders.identifier('lightSlottedContentMap'))
1775
+ : bNullishLightSlottedContentMap(builders.identifier('lightSlottedContentMap'));
1776
+ const scopedSlottedContentMap = hasScopedSlottedContent
1777
+ ? blightSlottedContentMap(builders.identifier('scopedSlottedContentMap'))
1778
+ : bNullishLightSlottedContentMap(builders.identifier('scopedSlottedContentMap'));
1779
+ return bGenerateSlottedContent(shadowSlottedContentFn, lightSlottedContentMap, scopedSlottedContentMap, lightSlotContent, scopedSlotContent);
1743
1780
  }
1744
1781
 
1745
1782
  /*
@@ -2286,13 +2323,72 @@ function createNewContext(templateOptions) {
2286
2323
  }
2287
2324
  return false;
2288
2325
  };
2326
+ const getLocalVars = () => localVarStack.flatMap((varsSet) => Array.from(varsSet));
2327
+ const hoistedStatements = {
2328
+ module: [],
2329
+ templateFn: [],
2330
+ };
2331
+ const hoistedModuleDedupe = new Set();
2332
+ const hoistedTemplateDedupe = new Set();
2333
+ const hoist = {
2334
+ // Anything added here will be inserted at the top of the compiled template's
2335
+ // JS module.
2336
+ module(stmt, optionalDedupeKey) {
2337
+ if (optionalDedupeKey) {
2338
+ if (hoistedModuleDedupe.has(optionalDedupeKey)) {
2339
+ return;
2340
+ }
2341
+ hoistedModuleDedupe.add(optionalDedupeKey);
2342
+ }
2343
+ hoistedStatements.module.push(stmt);
2344
+ },
2345
+ // Anything added here will be inserted at the top of the JavaScript function
2346
+ // corresponding to the template (typically named `__lwcTmpl`).
2347
+ templateFn(stmt, optionalDedupeKey) {
2348
+ if (optionalDedupeKey) {
2349
+ if (hoistedTemplateDedupe.has(optionalDedupeKey)) {
2350
+ return;
2351
+ }
2352
+ hoistedTemplateDedupe.add(optionalDedupeKey);
2353
+ }
2354
+ hoistedStatements.templateFn.push(stmt);
2355
+ },
2356
+ };
2357
+ const shadowSlotToFnName = new Map();
2358
+ let fnNameUniqueId = 0;
2359
+ // At present, we only track shadow-slotted content. This is because the functions
2360
+ // corresponding to shadow-slotted content are deduped and hoisted to the top of
2361
+ // the template function, whereas light-dom-slotted content is inlined. It may be
2362
+ // desirable to also track light-dom-slotted content at some future point in time.
2363
+ const slots = {
2364
+ shadow: {
2365
+ isDuplicate(uniqueNodeId) {
2366
+ return shadowSlotToFnName.has(uniqueNodeId);
2367
+ },
2368
+ register(uniqueNodeId, kebabCmpName) {
2369
+ if (slots.shadow.isDuplicate(uniqueNodeId)) {
2370
+ return shadowSlotToFnName.get(uniqueNodeId);
2371
+ }
2372
+ const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`;
2373
+ shadowSlotToFnName.set(uniqueNodeId, shadowSlotContentFnName);
2374
+ return shadowSlotContentFnName;
2375
+ },
2376
+ getFnName(uniqueNodeId) {
2377
+ return shadowSlotToFnName.get(uniqueNodeId) ?? null;
2378
+ },
2379
+ },
2380
+ };
2289
2381
  return {
2290
2382
  getImports: () => importManager.getImportDeclarations(),
2291
2383
  cxt: {
2292
2384
  pushLocalVars,
2293
2385
  popLocalVars,
2294
2386
  isLocalVar,
2387
+ getLocalVars,
2295
2388
  templateOptions,
2389
+ hoist,
2390
+ hoistedStatements,
2391
+ slots,
2296
2392
  import: importManager.add.bind(importManager),
2297
2393
  siblings: undefined,
2298
2394
  currentNodeIndex: undefined,
@@ -2405,6 +2501,7 @@ function templateIrToEsTree(node, contextOpts) {
2405
2501
  addImport: cxt.import,
2406
2502
  getImports,
2407
2503
  statements,
2504
+ cxt,
2408
2505
  };
2409
2506
  }
2410
2507
 
@@ -2476,6 +2573,7 @@ function compileTemplate(src, filename, options, compilationMode) {
2476
2573
  experimentalComputedMemberExpression: options.experimentalComputedMemberExpression,
2477
2574
  experimentalComplexExpressions: options.experimentalComplexExpressions,
2478
2575
  enableDynamicComponents: options.enableDynamicComponents,
2576
+ enableLwcOn: options.enableLwcOn,
2479
2577
  preserveHtmlComments: options.preserveHtmlComments,
2480
2578
  enableStaticContentOptimization: options.enableStaticContentOptimization,
2481
2579
  instrumentation: options.instrumentation,
@@ -2499,7 +2597,7 @@ function compileTemplate(src, filename, options, compilationMode) {
2499
2597
  }
2500
2598
  const preserveComments = !!root.directives.find((directive) => directive.name === 'PreserveComments')?.value?.value;
2501
2599
  const experimentalComplexExpressions = Boolean(options.experimentalComplexExpressions);
2502
- const { addImport, getImports, statements } = templateIrToEsTree(root, {
2600
+ const { addImport, getImports, statements, cxt } = templateIrToEsTree(root, {
2503
2601
  preserveComments,
2504
2602
  experimentalComplexExpressions,
2505
2603
  });
@@ -2507,7 +2605,14 @@ function compileTemplate(src, filename, options, compilationMode) {
2507
2605
  for (const [imports, source] of getStylesheetImports(filename)) {
2508
2606
  addImport(imports, source);
2509
2607
  }
2510
- let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts(statements));
2608
+ let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts([
2609
+ // Deep in the compiler, we may choose to hoist statements and declarations
2610
+ // to the top of the template function. After `templateIrToEsTree`, these
2611
+ // hoisted statements/declarations are prepended to the template function's
2612
+ // body.
2613
+ ...cxt.hoistedStatements.templateFn,
2614
+ ...statements,
2615
+ ]));
2511
2616
  // Ideally, we'd just do ${LWC_VERSION_COMMENT} in the code template,
2512
2617
  // but placeholders have a special meaning for `esTemplate`.
2513
2618
  tmplDecl = produce(tmplDecl, (draft) => {
@@ -2518,7 +2623,15 @@ function compileTemplate(src, filename, options, compilationMode) {
2518
2623
  },
2519
2624
  ];
2520
2625
  });
2521
- let program = builders.program([...getImports(), tmplDecl], 'module');
2626
+ let program = builders.program([
2627
+ // All import declarations come first...
2628
+ ...getImports(),
2629
+ // ... followed by any statements or declarations that need to be hoisted
2630
+ // to the top of the module scope...
2631
+ ...cxt.hoistedStatements.module,
2632
+ // ... followed by the template function declaration itself.
2633
+ tmplDecl,
2634
+ ], 'module');
2522
2635
  addScopeTokenDeclarations(program, filename, options.namespace, options.name);
2523
2636
  if (compilationMode === 'async' || compilationMode === 'sync') {
2524
2637
  program = transmogrify(program, compilationMode);
@@ -2548,5 +2661,5 @@ function compileTemplateForSSR(src, filename, options, mode = DEFAULT_SSR_MODE)
2548
2661
  }
2549
2662
 
2550
2663
  export { compileComponentForSSR, compileTemplateForSSR };
2551
- /** version: 8.19.0 */
2664
+ /** version: 8.20.0 */
2552
2665
  //# sourceMappingURL=index.js.map
package/dist/shared.d.ts CHANGED
@@ -1,36 +1,6 @@
1
1
  import type { LwcBabelPluginOptions } from '@lwc/babel-plugin-component';
2
2
  import type { Config as TemplateCompilerConfig } from '@lwc/template-compiler';
3
3
  export type Expression = string;
4
- export type Instruction = IEmitTagName | IEmitStaticString | IEmitExpression | IStartConditional | IEndConditional | IInvokeConnectedCallback | IRenderChild | IHoistImport | IHoistInstantiation;
5
- export interface IEmitTagName {
6
- kind: 'emitTagName';
7
- }
8
- export interface IEmitStaticString {
9
- kind: 'emitStaticString';
10
- }
11
- export interface IEmitExpression {
12
- kind: 'emitExpression';
13
- expression: Expression;
14
- }
15
- export interface IStartConditional {
16
- kind: 'startConditional';
17
- }
18
- export interface IEndConditional {
19
- kind: 'endConditional';
20
- }
21
- export interface IInvokeConnectedCallback {
22
- kind: 'invokeConnectedCallback';
23
- }
24
- export interface IRenderChild {
25
- kind: 'renderChild';
26
- dynamic: Expression | null;
27
- }
28
- export interface IHoistImport {
29
- kind: 'hoistImport';
30
- }
31
- export interface IHoistInstantiation {
32
- kind: 'hoistInstantiation';
33
- }
34
4
  export type TemplateTransformOptions = Pick<TemplateCompilerConfig, 'name' | 'namespace'>;
35
5
  export type ComponentTransformOptions = Partial<Pick<LwcBabelPluginOptions, 'name' | 'namespace'>> & {
36
6
  experimentalDynamicComponent?: LwcBabelPluginOptions['dynamicImports'];
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
5
5
  ],
6
6
  "name": "@lwc/ssr-compiler",
7
- "version": "8.19.0",
7
+ "version": "8.20.0",
8
8
  "description": "Compile component for use during server-side rendering",
9
9
  "keywords": [
10
10
  "compiler",
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@babel/types": "7.27.0",
52
- "@lwc/errors": "8.19.0",
53
- "@lwc/shared": "8.19.0",
54
- "@lwc/template-compiler": "8.19.0",
52
+ "@lwc/errors": "8.20.0",
53
+ "@lwc/shared": "8.20.0",
54
+ "@lwc/template-compiler": "8.20.0",
55
55
  "acorn": "8.14.1",
56
56
  "astring": "^1.9.0",
57
57
  "estree-toolkit": "^1.7.12",
@@ -59,7 +59,7 @@
59
59
  "meriyah": "^5.0.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@lwc/babel-plugin-component": "8.19.0",
62
+ "@lwc/babel-plugin-component": "8.20.0",
63
63
  "@types/estree": "^1.0.7"
64
64
  }
65
65
  }