@lwc/template-compiler 6.3.3 → 6.4.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.
package/dist/index.cjs.js CHANGED
@@ -75,7 +75,10 @@ const DASHED_TAGNAME_ELEMENT_SET = new Set([
75
75
  'missing-glyph',
76
76
  ]);
77
77
  // Subset of LWC template directives that can safely be statically optimized
78
- const STATIC_SAFE_DIRECTIVES = new Set(['Ref']);
78
+ const STATIC_SAFE_DIRECTIVES = new Set([
79
+ 'Ref',
80
+ 'Key',
81
+ ]);
79
82
 
80
83
  /*
81
84
  * Copyright (c) 2024, Salesforce, Inc.
@@ -87,7 +90,7 @@ function toPropertyName(attr) {
87
90
  let prop = '';
88
91
  let shouldUpperCaseNext = false;
89
92
  for (let i = 0; i < attr.length; i++) {
90
- const char = attr.charAt(i);
93
+ const char = shared.StringCharAt.call(attr, i);
91
94
  if (char === '-') {
92
95
  shouldUpperCaseNext = true;
93
96
  }
@@ -9317,6 +9320,9 @@ function isProperty(node) {
9317
9320
  function isScopedSlotFragment(node) {
9318
9321
  return node.type === 'ScopedSlotFragment';
9319
9322
  }
9323
+ function isAttribute$1(node) {
9324
+ return node.type === 'Attribute';
9325
+ }
9320
9326
 
9321
9327
  exports.LWCDirectiveDomMode = void 0;
9322
9328
  (function (LWCDirectiveDomMode) {
@@ -12602,6 +12608,18 @@ function parseClassNames(classNames) {
12602
12608
  .map((className) => className.trim())
12603
12609
  .filter((className) => className.length);
12604
12610
  }
12611
+
12612
+ /*
12613
+ * Copyright (c) 2024, salesforce.com, inc.
12614
+ * All rights reserved.
12615
+ * SPDX-License-Identifier: MIT
12616
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
12617
+ */
12618
+ // This set keeps track of static safe elements that have dynamic text in their direct children.
12619
+ const STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET = new WeakSet();
12620
+ // This map keeps track of static safe elements to their transformed children.
12621
+ // The children are transformed so that contiguous text nodes are consolidated into arrays.
12622
+ const STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE = new WeakMap();
12605
12623
  function isStaticNode(node, apiVersion) {
12606
12624
  let result = true;
12607
12625
  const { name: nodeName, namespace = '', attributes, directives, properties } = node;
@@ -12614,8 +12632,9 @@ function isStaticNode(node, apiVersion) {
12614
12632
  // it is an element
12615
12633
  result &&= isElement(node);
12616
12634
  // all attrs are static-safe
12635
+ // the criteria to determine safety can be found in computeAttrValue
12617
12636
  result &&= attributes.every(({ name, value }) => {
12618
- return (isLiteral(value) &&
12637
+ const isStaticSafeLiteral = isLiteral(value) &&
12619
12638
  name !== 'slot' &&
12620
12639
  // check for ScopedId
12621
12640
  name !== 'id' &&
@@ -12625,7 +12644,13 @@ function isStaticNode(node, apiVersion) {
12625
12644
  !isSvgUseHref(nodeName, name, namespace) &&
12626
12645
  // Check for ScopedFragId
12627
12646
  !(isAllowedFragOnlyUrlsXHTML(nodeName, name, namespace) &&
12628
- isFragmentOnlyUrl(value.value)));
12647
+ isFragmentOnlyUrl(value.value));
12648
+ const isStaticSafeExpression = isExpression$1(value) &&
12649
+ name !== 'slot' &&
12650
+ // TODO [#3624]: Revisit whether svgs can be included in static content optimization
12651
+ // svg href needs sanitization.
12652
+ !isSvgUseHref(nodeName, name, namespace);
12653
+ return isStaticSafeLiteral || isStaticSafeExpression;
12629
12654
  });
12630
12655
  // all directives are static-safe
12631
12656
  result &&= !directives.some((directive) => !STATIC_SAFE_DIRECTIVES.has(directive.name));
@@ -12634,30 +12659,40 @@ function isStaticNode(node, apiVersion) {
12634
12659
  return result;
12635
12660
  }
12636
12661
  function collectStaticNodes(node, staticNodes, state) {
12637
- let childrenAreStatic = true;
12638
- let nodeIsStatic;
12662
+ let childrenAreStaticSafe = true;
12663
+ let nodeIsStaticSafe;
12639
12664
  if (isText(node)) {
12640
- nodeIsStatic = isLiteral(node.value);
12665
+ nodeIsStaticSafe = true;
12641
12666
  }
12642
12667
  else if (isComment(node)) {
12643
- nodeIsStatic = true;
12668
+ nodeIsStaticSafe = true;
12644
12669
  }
12645
12670
  else {
12671
+ let hasDynamicText = false;
12646
12672
  // it is ElseBlock | ForBlock | If | BaseElement
12647
12673
  node.children.forEach((childNode) => {
12648
12674
  collectStaticNodes(childNode, staticNodes, state);
12649
- childrenAreStatic &&= staticNodes.has(childNode);
12675
+ childrenAreStaticSafe &&= staticNodes.has(childNode);
12676
+ // Collect nodes that have dynamic text ahead of time.
12677
+ // We only need to know if the direct child has dynamic text.
12678
+ hasDynamicText ||= isText(childNode) && !isStringLiteral(childNode.value);
12650
12679
  });
12651
12680
  // for IfBlock and ElseifBlock, traverse down the else branch
12652
12681
  if (isConditionalParentBlock(node) && node.else) {
12653
12682
  collectStaticNodes(node.else, staticNodes, state);
12654
12683
  }
12655
- nodeIsStatic =
12684
+ nodeIsStaticSafe =
12656
12685
  isBaseElement(node) &&
12657
12686
  !isCustomRendererHookRequired(node, state) &&
12658
12687
  isStaticNode(node, state.config.apiVersion);
12688
+ if (nodeIsStaticSafe && hasDynamicText) {
12689
+ // Track when the static element contains dynamic text.
12690
+ // This will alter the way the children need to be traversed to apply static parts.
12691
+ // See transformStaticChildren below.
12692
+ STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET.add(node);
12693
+ }
12659
12694
  }
12660
- if (nodeIsStatic && childrenAreStatic) {
12695
+ if (nodeIsStaticSafe && childrenAreStaticSafe) {
12661
12696
  staticNodes.add(node);
12662
12697
  }
12663
12698
  }
@@ -12668,6 +12703,62 @@ function getStaticNodes(root, state) {
12668
12703
  });
12669
12704
  return staticNodes;
12670
12705
  }
12706
+ // The purpose of this function is to concatenate the contiguous text nodes into a single array
12707
+ // to simplify the traversing logic when generating static parts and serializing the element.
12708
+ function transformStaticChildren(elm) {
12709
+ const children = elm.children;
12710
+ if (!children.length || !STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET.has(elm)) {
12711
+ // The element either has no children or its children does not contain dynamic text.
12712
+ return children;
12713
+ }
12714
+ if (STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.has(elm)) {
12715
+ return STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.get(elm);
12716
+ }
12717
+ const result = [];
12718
+ const len = children.length;
12719
+ let current;
12720
+ let next;
12721
+ let contiguousTextNodes = null;
12722
+ for (let i = 0; i < len; i++) {
12723
+ current = children[i];
12724
+ if (!isText(current)) {
12725
+ contiguousTextNodes = null;
12726
+ result.push(current);
12727
+ }
12728
+ else {
12729
+ if (!shared.isNull(contiguousTextNodes)) {
12730
+ // Already in a contiguous text node chain
12731
+ // All contiguous nodes represent an expression in the source, it's guaranteed by the parser.
12732
+ contiguousTextNodes.push(current);
12733
+ }
12734
+ else {
12735
+ next = children[i + 1];
12736
+ if (isExpression$1(current) || (!shared.isUndefined(next) && isText(next))) {
12737
+ // Text nodes can appear as follows:
12738
+ // 1. A single text literal node.
12739
+ // 2. A single text expression node.
12740
+ // 3. Contiguous series of text nodes (literal/expression mixed) with at least 1 expression.
12741
+ // When there is an expression in the source, the text nodes are split into contiguous text nodes.
12742
+ // When there is no expression in the source, the text will appear as a single text literal.
12743
+ // We normalize all of the contiguous text nodes or single text expression to an array.
12744
+ // Single text literal nodes (no expression or are not part of a contiguous set of text nodes) remain text nodes
12745
+ // and will not be consolidated to an array.
12746
+ // This is to normalize the traversal behavior when creating static parts and when serializing
12747
+ // the elements.
12748
+ contiguousTextNodes = [current];
12749
+ }
12750
+ // When contiguousTextNodes is null it is a single string literal.
12751
+ result.push(contiguousTextNodes ?? current);
12752
+ }
12753
+ }
12754
+ }
12755
+ STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.set(elm, result);
12756
+ return result;
12757
+ }
12758
+ // Dynamic text is consolidated from individual text arrays into a single Text[].
12759
+ // Static text = a single text literal node (not in an array).
12760
+ // Dynamic text = At least 1 text expression node + 0 or more text literal nodes (always in an array).
12761
+ const isDynamicText = (nodes) => shared.isArray(nodes) && shared.ArrayEvery.call(nodes, isText);
12671
12762
 
12672
12763
  /*
12673
12764
  * Copyright (c) 2018, salesforce.com, inc.
@@ -12694,7 +12785,7 @@ const rawContentElements = new Set([
12694
12785
  function templateStringEscape(str) {
12695
12786
  return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
12696
12787
  }
12697
- function serializeAttrs(element) {
12788
+ function serializeAttrs(element, codeGen) {
12698
12789
  /**
12699
12790
  * 0: styleToken in existing class attr
12700
12791
  * 1: styleToken for added class attr
@@ -12702,24 +12793,36 @@ function serializeAttrs(element) {
12702
12793
  */
12703
12794
  const attrs = [];
12704
12795
  let hasClassAttr = false;
12705
- const collector = ({ name, value }) => {
12796
+ const collector = ({ name, value, hasExpression, }) => {
12706
12797
  let v = typeof value === 'string' ? templateStringEscape(value) : value;
12707
12798
  if (name === 'class') {
12708
12799
  hasClassAttr = true;
12709
- v += '${0}';
12800
+ // ${0} maps to class token that will be appended to the string.
12801
+ // See buildParseFragmentFn for details.
12802
+ // The token is only needed when the class attribute is static.
12803
+ // The token will be injected at runtime for expressions in parseFragmentFn.
12804
+ if (!hasExpression) {
12805
+ v += '${0}';
12806
+ }
12710
12807
  }
12711
12808
  if (typeof v === 'string') {
12712
- attrs.push(`${name}="${shared.htmlEscape(v, true)}"`);
12809
+ // Inject a placeholder where the staticPartId will go when an expression occurs.
12810
+ // This is only needed for SSR to inject the expression value during serialization.
12811
+ attrs.push(hasExpression ? `\${"${v}"}` : ` ${name}="${shared.htmlEscape(v, true)}"`);
12713
12812
  }
12714
12813
  else {
12715
- attrs.push(name);
12814
+ attrs.push(` ${name}`);
12716
12815
  }
12717
12816
  };
12718
12817
  element.attributes
12719
12818
  .map((attr) => {
12819
+ const hasExpression = isExpression$1(attr.value);
12720
12820
  return {
12821
+ hasExpression,
12721
12822
  name: attr.name,
12722
- value: attr.value.value,
12823
+ value: hasExpression
12824
+ ? codeGen.getStaticExpressionToken(attr)
12825
+ : attr.value.value,
12723
12826
  };
12724
12827
  })
12725
12828
  .forEach(collector);
@@ -12735,31 +12838,42 @@ function serializeAttrs(element) {
12735
12838
  };
12736
12839
  })
12737
12840
  .forEach(collector);
12738
- return (attrs.length > 0 ? ' ' : '') + attrs.join(' ') + (hasClassAttr ? '${2}' : '${3}');
12841
+ // ${2} maps to style token attribute
12842
+ // ${3} maps to class attribute token + style token attribute
12843
+ // See buildParseFragmentFn for details.
12844
+ return attrs.join('') + (hasClassAttr ? '${2}' : '${3}');
12739
12845
  }
12740
- function serializeChildren(children, parentTagName, preserveComments) {
12846
+ function serializeChildren(node, parentTagName, codeGen) {
12741
12847
  let html = '';
12742
- children.forEach((child) => {
12848
+ for (const child of transformStaticChildren(node)) {
12743
12849
  /* istanbul ignore else */
12744
- if (isElement(child)) {
12745
- html += serializeStaticElement(child, preserveComments);
12850
+ if (isDynamicText(child)) {
12851
+ html += serializeDynamicTextNode(child, codeGen);
12746
12852
  }
12747
12853
  else if (isText(child)) {
12748
- html += serializeTextNode(child, rawContentElements.has(parentTagName.toUpperCase()));
12854
+ html += serializeStaticTextNode(child, rawContentElements.has(parentTagName.toUpperCase()));
12855
+ }
12856
+ else if (isElement(child)) {
12857
+ html += serializeStaticElement(child, codeGen);
12749
12858
  }
12750
12859
  else if (isComment(child)) {
12751
- html += serializeCommentNode(child, preserveComments);
12860
+ html += serializeCommentNode(child, codeGen.preserveComments);
12752
12861
  }
12753
12862
  else {
12754
12863
  throw new TypeError('Unknown node found while serializing static content. Allowed nodes types are: Element, Text and Comment.');
12755
12864
  }
12756
- });
12865
+ }
12757
12866
  return html;
12758
12867
  }
12759
12868
  function serializeCommentNode(comment, preserveComment) {
12760
12869
  return preserveComment ? `<!--${shared.htmlEscape(templateStringEscape(comment.value))}-->` : '';
12761
12870
  }
12762
- function serializeTextNode(text, useRawContent) {
12871
+ function serializeDynamicTextNode(textNodes, codeGen) {
12872
+ // The first text node is they key for contiguous text nodes and single expressions.
12873
+ // This is guaranteed to have a value by the isDynamicText check.
12874
+ return `\${"${codeGen.getStaticExpressionToken(textNodes[0])}"}`;
12875
+ }
12876
+ function serializeStaticTextNode(text, useRawContent) {
12763
12877
  let content;
12764
12878
  if (useRawContent) {
12765
12879
  content = text.raw;
@@ -12767,19 +12881,20 @@ function serializeTextNode(text, useRawContent) {
12767
12881
  else {
12768
12882
  content = shared.htmlEscape(text.value.value);
12769
12883
  }
12770
- return templateStringEscape(content);
12884
+ content = templateStringEscape(content);
12885
+ return content;
12771
12886
  }
12772
- function serializeStaticElement(element, preserveComments) {
12887
+ function serializeStaticElement(element, codeGen) {
12773
12888
  const { name: tagName, namespace } = element;
12774
12889
  const isForeignElement = namespace !== shared.HTML_NAMESPACE;
12775
12890
  const hasChildren = element.children.length > 0;
12776
- let html = `<${tagName}${serializeAttrs(element)}`;
12891
+ let html = `<${tagName}${serializeAttrs(element, codeGen)}`;
12777
12892
  if (isForeignElement && !hasChildren) {
12778
12893
  html += '/>';
12779
12894
  return html;
12780
12895
  }
12781
12896
  html += '>';
12782
- html += serializeChildren(element.children, tagName, preserveComments);
12897
+ html += serializeChildren(element, tagName, codeGen);
12783
12898
  if (!shared.isVoidElement(tagName, namespace) || hasChildren) {
12784
12899
  html += `</${tagName}>`;
12785
12900
  }
@@ -12931,6 +13046,42 @@ function collectParamsFromMemberExpression(_node, _vars) {
12931
13046
  // the AST, we'll validate anyway.
12932
13047
  errors.invariant(false, errors.ParserDiagnostics.INVALID_EXPR_ARROW_FN_PARAM, ['member expressions']);
12933
13048
  }
13049
+ function bindAttributeExpression(attr, element, codeGen, addLegacySanitizationHook) {
13050
+ const { name: elmName, namespace = '' } = element;
13051
+ const { value: attrValue } = attr;
13052
+ // Evaluate properties based on their attribute name
13053
+ const attrName = isProperty(attr) ? attr.attributeName : attr.name;
13054
+ const isUsedAsAttribute = isAttribute(element, attrName);
13055
+ const expression = codeGen.bindExpression(attrValue);
13056
+ // TODO [#2012]: Normalize global boolean attrs values passed to custom elements as props
13057
+ if (isUsedAsAttribute && shared.isBooleanAttribute(attrName, elmName)) {
13058
+ // We need to do some manipulation to allow the diffing algorithm add/remove the attribute
13059
+ // without handling special cases at runtime.
13060
+ return codeGen.genBooleanAttributeExpr(expression);
13061
+ }
13062
+ if (attrName === 'tabindex') {
13063
+ return codeGen.genTabIndex([expression]);
13064
+ }
13065
+ if (attrName === 'id' || isIdReferencingAttribute(attrName)) {
13066
+ return codeGen.genScopedId(expression);
13067
+ }
13068
+ if (codeGen.scopeFragmentId && isAllowedFragOnlyUrlsXHTML(elmName, attrName, namespace)) {
13069
+ return codeGen.genScopedFragId(expression);
13070
+ }
13071
+ if (isSvgUseHref(elmName, attrName, namespace)) {
13072
+ if (addLegacySanitizationHook) {
13073
+ codeGen.usedLwcApis.add('sanitizeAttribute');
13074
+ return callExpression(identifier('sanitizeAttribute'), [
13075
+ literal$1(elmName),
13076
+ literal$1(namespace),
13077
+ literal$1(attrName),
13078
+ codeGen.genScopedFragId(expression),
13079
+ ]);
13080
+ }
13081
+ return codeGen.genScopedFragId(expression);
13082
+ }
13083
+ return expression;
13084
+ }
12934
13085
 
12935
13086
  /*
12936
13087
  * Copyright (c) 2018, salesforce.com, inc.
@@ -12944,27 +13095,27 @@ const doStructuredClone = typeof structuredClone === 'function'
12944
13095
  ? structuredClone
12945
13096
  : (obj) => JSON.parse(JSON.stringify(obj));
12946
13097
  const RENDER_APIS = {
12947
- iterator: { name: 'i', alias: 'api_iterator' },
12948
- flatten: { name: 'f', alias: 'api_flatten' },
12949
- element: { name: 'h', alias: 'api_element' },
12950
- slot: { name: 's', alias: 'api_slot' },
13098
+ bind: { name: 'b', alias: 'api_bind' },
13099
+ comment: { name: 'co', alias: 'api_comment' },
12951
13100
  customElement: { name: 'c', alias: 'api_custom_element' },
12952
- dynamicCtor: { name: 'dc', alias: 'api_dynamic_component' },
12953
13101
  // TODO [#3331]: remove usage of lwc:dynamic in 246
12954
13102
  deprecatedDynamicCtor: { name: 'ddc', alias: 'api_deprecated_dynamic_component' },
12955
- bind: { name: 'b', alias: 'api_bind' },
12956
- text: { name: 't', alias: 'api_text' },
13103
+ dynamicCtor: { name: 'dc', alias: 'api_dynamic_component' },
12957
13104
  dynamicText: { name: 'd', alias: 'api_dynamic_text' },
13105
+ element: { name: 'h', alias: 'api_element' },
13106
+ flatten: { name: 'f', alias: 'api_flatten' },
13107
+ fragment: { name: 'fr', alias: 'api_fragment' },
13108
+ iterator: { name: 'i', alias: 'api_iterator' },
12958
13109
  key: { name: 'k', alias: 'api_key' },
12959
- tabindex: { name: 'ti', alias: 'api_tab_index' },
12960
- scopedId: { name: 'gid', alias: 'api_scoped_id' },
12961
- scopedFragId: { name: 'fid', alias: 'api_scoped_frag_id' },
12962
- comment: { name: 'co', alias: 'api_comment' },
12963
13110
  sanitizeHtmlContent: { name: 'shc', alias: 'api_sanitize_html_content' },
12964
- fragment: { name: 'fr', alias: 'api_fragment' },
12965
- staticFragment: { name: 'st', alias: 'api_static_fragment' },
13111
+ scopedFragId: { name: 'fid', alias: 'api_scoped_frag_id' },
13112
+ scopedId: { name: 'gid', alias: 'api_scoped_id' },
12966
13113
  scopedSlotFactory: { name: 'ssf', alias: 'api_scoped_slot_factory' },
13114
+ slot: { name: 's', alias: 'api_slot' },
13115
+ staticFragment: { name: 'st', alias: 'api_static_fragment' },
12967
13116
  staticPart: { name: 'sp', alias: 'api_static_part' },
13117
+ tabindex: { name: 'ti', alias: 'api_tab_index' },
13118
+ text: { name: 't', alias: 'api_text' },
12968
13119
  };
12969
13120
  class CodeGen {
12970
13121
  constructor({ root, state, scopeFragmentId, }) {
@@ -12980,6 +13131,7 @@ class CodeGen {
12980
13131
  this.slotNames = new Set();
12981
13132
  this.memorizedIds = [];
12982
13133
  this.referencedComponents = new Set();
13134
+ this.staticExpressionMap = new WeakMap();
12983
13135
  this.root = root;
12984
13136
  if (state.config.enableStaticContentOptimization) {
12985
13137
  this.staticNodes = getStaticNodes(root, state);
@@ -13028,6 +13180,9 @@ class CodeGen {
13028
13180
  return this._renderApiCall(RENDER_APIS.deprecatedDynamicCtor, args);
13029
13181
  }
13030
13182
  genText(value) {
13183
+ return this._renderApiCall(RENDER_APIS.text, [this.genConcatenatedText(value)]);
13184
+ }
13185
+ genConcatenatedText(value) {
13031
13186
  const mappedValues = value.map((v) => {
13032
13187
  return typeof v === 'string'
13033
13188
  ? literal$1(v)
@@ -13037,7 +13192,7 @@ class CodeGen {
13037
13192
  for (let i = 1, n = mappedValues.length; i < n; i++) {
13038
13193
  textConcatenation = binaryExpression('+', textConcatenation, mappedValues[i]);
13039
13194
  }
13040
- return this._renderApiCall(RENDER_APIS.text, [textConcatenation]);
13195
+ return textConcatenation;
13041
13196
  }
13042
13197
  genComment(value) {
13043
13198
  return this._renderApiCall(RENDER_APIS.comment, [literal$1(value)]);
@@ -13058,9 +13213,6 @@ class CodeGen {
13058
13213
  genFlatten(children) {
13059
13214
  return this._renderApiCall(RENDER_APIS.flatten, children);
13060
13215
  }
13061
- genKey(compilerKey, value) {
13062
- return this._renderApiCall(RENDER_APIS.key, [compilerKey, value]);
13063
- }
13064
13216
  genScopedId(id) {
13065
13217
  if (typeof id === 'string') {
13066
13218
  return this._renderApiCall(RENDER_APIS.scopedId, [literal$1(id)]);
@@ -13121,6 +13273,28 @@ class CodeGen {
13121
13273
  this.hasRefs = true;
13122
13274
  return property$1(identifier('ref'), ref.value);
13123
13275
  }
13276
+ genKeyExpression(ref, slotParentName) {
13277
+ if (ref) {
13278
+ // If element has user-supplied `key` or is in iterator, call `api.k`
13279
+ const forKeyExpression = this.bindExpression(ref.value);
13280
+ const key = this.generateKey();
13281
+ return this._renderApiCall(RENDER_APIS.key, [literal$1(key), forKeyExpression]);
13282
+ }
13283
+ else {
13284
+ // If standalone element with no user-defined key
13285
+ let key = this.generateKey();
13286
+ // Parent slot name could be the empty string
13287
+ if (slotParentName !== undefined) {
13288
+ // Prefixing the key is necessary to avoid conflicts with default content for the
13289
+ // slot which might have similar keys. Each vnode will always have a key that starts
13290
+ // with a numeric character from compiler. In this case, we add a unique notation
13291
+ // for slotted vnodes keys, e.g.: `@foo:1:1`. Note that this is *not* needed for
13292
+ // dynamic keys, since `api.k` already scopes based on the iteration.
13293
+ key = `@${slotParentName}:${key}`;
13294
+ }
13295
+ return literal$1(key);
13296
+ }
13297
+ }
13124
13298
  /**
13125
13299
  * This routine generates an expression that avoids
13126
13300
  * computing the sanitized html of a raw html if it does not change
@@ -13230,10 +13404,9 @@ class CodeGen {
13230
13404
  return expression;
13231
13405
  }
13232
13406
  genStaticElement(element, slotParentName) {
13233
- const key = slotParentName !== undefined
13234
- ? `@${slotParentName}:${this.generateKey()}`
13235
- : this.generateKey();
13236
- const html = serializeStaticElement(element, this.preserveComments);
13407
+ const staticParts = this.genStaticParts(element);
13408
+ // Generate static parts prior to serialization to inject the corresponding static part Id into the serialized output.
13409
+ const html = serializeStaticElement(element, this);
13237
13410
  const parseMethod = element.name !== 'svg' && element.namespace === shared.SVG_NAMESPACE
13238
13411
  ? PARSE_SVG_FRAGMENT_METHOD_NAME
13239
13412
  : PARSE_FRAGMENT_METHOD_NAME;
@@ -13254,9 +13427,14 @@ class CodeGen {
13254
13427
  identifier: identifier$1,
13255
13428
  expr,
13256
13429
  });
13257
- const args = [callExpression(identifier$1, []), literal$1(key)];
13430
+ // Keys are only supported at the top level of a static block, and are serialized directly in the args for
13431
+ // the `api_static_fragment` call. We don't need to support keys in static parts (i.e. children of
13432
+ // the top-level element), because the compiler ignores any keys that aren't direct children of a
13433
+ // for:each block (see error code 1149 - "KEY_SHOULD_BE_IN_ITERATION").
13434
+ const key = element.directives.find(isKeyDirective);
13435
+ const keyExpression = this.genKeyExpression(key, slotParentName);
13436
+ const args = [identifier$1, keyExpression];
13258
13437
  // Only add the third argument (staticParts) if this element needs it
13259
- const staticParts = this.genStaticParts(element);
13260
13438
  if (staticParts) {
13261
13439
  args.push(staticParts);
13262
13440
  }
@@ -13264,52 +13442,106 @@ class CodeGen {
13264
13442
  }
13265
13443
  genStaticParts(element) {
13266
13444
  const stack = [element];
13267
- const partIdsToDatabagProps = new Map();
13445
+ const partIdsToArgs = new Map();
13268
13446
  let partId = -1;
13269
- const addDatabagProp = (prop) => {
13270
- let databags = partIdsToDatabagProps.get(partId);
13271
- if (!databags) {
13272
- databags = [];
13273
- partIdsToDatabagProps.set(partId, databags);
13447
+ const getPartIdArgs = (partId) => {
13448
+ let args = partIdsToArgs.get(partId);
13449
+ if (!args) {
13450
+ args = { text: literal$1(null), databag: literal$1(null) };
13451
+ partIdsToArgs.set(partId, args);
13274
13452
  }
13275
- databags.push(prop);
13453
+ return args;
13454
+ };
13455
+ const setPartIdText = (text) => {
13456
+ const args = getPartIdArgs(partId);
13457
+ args.text = text;
13458
+ };
13459
+ const setPartIdDatabag = (databag) => {
13460
+ const args = getPartIdArgs(partId);
13461
+ args.databag = objectExpression(databag);
13276
13462
  };
13277
13463
  // Depth-first traversal. We assign a partId to each element, which is an integer based on traversal order.
13278
13464
  while (stack.length > 0) {
13279
- const node = stack.shift();
13465
+ const current = stack.shift();
13280
13466
  // Skip comment nodes in parts count, as they will be stripped in production, unless when `lwc:preserve-comments` is enabled
13281
- if (!isComment(node) || this.preserveComments) {
13467
+ if (isDynamicText(current) || !isComment(current) || this.preserveComments) {
13282
13468
  partId++;
13283
13469
  }
13284
- if (isElement(node)) {
13470
+ if (isDynamicText(current)) {
13471
+ const textNodes = current;
13472
+ const partToken = `${"t" /* STATIC_PART_TOKEN_ID.TEXT */}${partId}`;
13473
+ // Use the first text node as the key.
13474
+ // Dynamic text is guaranteed to have at least 1 text node in the array by transformStaticChildren.
13475
+ this.staticExpressionMap.set(textNodes[0], partToken);
13476
+ const concatenatedText = this.genConcatenatedText(textNodes.map(({ value }) => isStringLiteral(value) ? value.value : this.bindExpression(value)));
13477
+ setPartIdText(concatenatedText);
13478
+ }
13479
+ else if (isElement(current)) {
13480
+ const elm = current;
13481
+ const databag = [];
13285
13482
  // has event listeners
13286
- if (node.listeners.length) {
13287
- addDatabagProp(this.genEventListeners(node.listeners));
13483
+ if (elm.listeners.length) {
13484
+ databag.push(this.genEventListeners(elm.listeners));
13288
13485
  }
13289
- // see STATIC_SAFE_DIRECTIVES for what's allowed here
13290
- for (const directive of node.directives) {
13486
+ // See STATIC_SAFE_DIRECTIVES for what's allowed here.
13487
+ // Also note that we don't generate the 'key' here, because we only support it at the top level
13488
+ // directly passed into the `api_static_fragment` function, not as a part.
13489
+ for (const directive of elm.directives) {
13291
13490
  if (directive.name === 'Ref') {
13292
- addDatabagProp(this.genRef(directive));
13491
+ databag.push(this.genRef(directive));
13293
13492
  }
13294
13493
  }
13295
- // For depth-first traversal, children must be preprended in order, so that they are processed before
13494
+ const attributeExpressions = [];
13495
+ for (const attribute of elm.attributes) {
13496
+ const { name, value } = attribute;
13497
+ if (isExpression$1(value)) {
13498
+ let partToken = '';
13499
+ if (name === 'style') {
13500
+ partToken = `${"s" /* STATIC_PART_TOKEN_ID.STYLE */}${partId}`;
13501
+ databag.push(property$1(identifier('style'), this.bindExpression(value)));
13502
+ }
13503
+ else if (name === 'class') {
13504
+ partToken = `${"c" /* STATIC_PART_TOKEN_ID.CLASS */}${partId}`;
13505
+ databag.push(property$1(identifier('className'), this.bindExpression(value)));
13506
+ }
13507
+ else {
13508
+ partToken = `${"a" /* STATIC_PART_TOKEN_ID.ATTRIBUTE */}${partId}:${name}`;
13509
+ attributeExpressions.push(property$1(literal$1(name), bindAttributeExpression(attribute, elm, this, false)));
13510
+ }
13511
+ this.staticExpressionMap.set(attribute, partToken);
13512
+ }
13513
+ }
13514
+ if (attributeExpressions.length) {
13515
+ databag.push(property$1(identifier('attrs'), objectExpression(attributeExpressions)));
13516
+ }
13517
+ if (databag.length) {
13518
+ setPartIdDatabag(databag);
13519
+ }
13520
+ // For depth-first traversal, children must be prepended in order, so that they are processed before
13296
13521
  // siblings. Note that this is consistent with the order used in the diffing algo as well as
13297
13522
  // `traverseAndSetElements` in @lwc/engine-core.
13298
- stack.unshift(...node.children);
13523
+ stack.unshift(...transformStaticChildren(elm));
13299
13524
  }
13300
13525
  }
13301
- if (partIdsToDatabagProps.size === 0) {
13302
- return undefined; // no databags needed
13526
+ if (partIdsToArgs.size === 0) {
13527
+ return undefined; // no parts needed
13303
13528
  }
13304
- return arrayExpression([...partIdsToDatabagProps.entries()].map(([partId, databagProperties]) => {
13305
- return this.genStaticPart(partId, databagProperties);
13529
+ return arrayExpression([...partIdsToArgs.entries()].map(([partId, { databag, text }]) => {
13530
+ return this.genStaticPart(partId, databag, text);
13306
13531
  }));
13307
13532
  }
13308
- genStaticPart(partId, databagProperties) {
13309
- return this._renderApiCall(RENDER_APIS.staticPart, [
13310
- literal$1(partId),
13311
- objectExpression(databagProperties),
13312
- ]);
13533
+ genStaticPart(partId, data, text) {
13534
+ return this._renderApiCall(RENDER_APIS.staticPart, [literal$1(partId), data, text]);
13535
+ }
13536
+ getStaticExpressionToken(node) {
13537
+ const token = this.staticExpressionMap.get(node);
13538
+ /* istanbul ignore if */
13539
+ if (shared.isUndefined(token)) {
13540
+ // It should not be possible to hit this code path
13541
+ const nodeName = isAttribute$1(node) ? node.name : 'text node';
13542
+ throw new Error(`Template compiler internal error, unable to map ${nodeName} to a static expression.`);
13543
+ }
13544
+ return token;
13313
13545
  }
13314
13546
  }
13315
13547
 
@@ -13500,13 +13732,15 @@ function format(templateFn, codeGen) {
13500
13732
  function transform(codeGen) {
13501
13733
  const instrumentation = codeGen.state.config.instrumentation;
13502
13734
  function transformElement(element, slotParentName) {
13735
+ // TODO [#4077]: Move databag gathering to after static element check as it doesn't seem to be used by static
13736
+ // content optimization.
13503
13737
  const databag = elementDataBag(element, slotParentName);
13504
- let res;
13505
13738
  if (codeGen.staticNodes.has(element) && isElement(element)) {
13506
13739
  // do not process children of static nodes.
13507
13740
  return codeGen.genStaticElement(element, slotParentName);
13508
13741
  }
13509
13742
  const children = transformChildren(element);
13743
+ let res;
13510
13744
  const { name } = element;
13511
13745
  // lwc:dynamic directive
13512
13746
  const deprecatedDynamicDirective = element.directives.find(isDynamicDirective);
@@ -13746,36 +13980,7 @@ function transform(codeGen) {
13746
13980
  const attrName = isProperty(attr) ? attr.attributeName : attr.name;
13747
13981
  const isUsedAsAttribute = isAttribute(element, attrName);
13748
13982
  if (isExpression$1(attrValue)) {
13749
- const expression = codeGen.bindExpression(attrValue);
13750
- // TODO [#2012]: Normalize global boolean attrs values passed to custom elements as props
13751
- if (isUsedAsAttribute && shared.isBooleanAttribute(attrName, elmName)) {
13752
- // We need to do some manipulation to allow the diffing algorithm add/remove the attribute
13753
- // without handling special cases at runtime.
13754
- return codeGen.genBooleanAttributeExpr(expression);
13755
- }
13756
- if (attrName === 'tabindex') {
13757
- return codeGen.genTabIndex([expression]);
13758
- }
13759
- if (attrName === 'id' || isIdReferencingAttribute(attrName)) {
13760
- return codeGen.genScopedId(expression);
13761
- }
13762
- if (codeGen.scopeFragmentId &&
13763
- isAllowedFragOnlyUrlsXHTML(elmName, attrName, namespace)) {
13764
- return codeGen.genScopedFragId(expression);
13765
- }
13766
- if (isSvgUseHref(elmName, attrName, namespace)) {
13767
- if (addLegacySanitizationHook) {
13768
- codeGen.usedLwcApis.add('sanitizeAttribute');
13769
- return callExpression(identifier('sanitizeAttribute'), [
13770
- literal$1(elmName),
13771
- literal$1(namespace),
13772
- literal$1(attrName),
13773
- codeGen.genScopedFragId(expression),
13774
- ]);
13775
- }
13776
- return codeGen.genScopedFragId(expression);
13777
- }
13778
- return expression;
13983
+ return bindAttributeExpression(attr, element, codeGen, addLegacySanitizationHook);
13779
13984
  }
13780
13985
  else if (isStringLiteral(attrValue)) {
13781
13986
  if (attrName === 'id') {
@@ -13926,26 +14131,7 @@ function transform(codeGen) {
13926
14131
  data.push(property$1(identifier('context'), contextObj));
13927
14132
  }
13928
14133
  // Key property on VNode
13929
- if (forKey) {
13930
- // If element has user-supplied `key` or is in iterator, call `api.k`
13931
- const forKeyExpression = codeGen.bindExpression(forKey.value);
13932
- const generatedKey = codeGen.genKey(literal$1(codeGen.generateKey()), forKeyExpression);
13933
- data.push(property$1(identifier('key'), generatedKey));
13934
- }
13935
- else {
13936
- // If standalone element with no user-defined key
13937
- let key = codeGen.generateKey();
13938
- // Parent slot name could be the empty string
13939
- if (slotParentName !== undefined) {
13940
- // Prefixing the key is necessary to avoid conflicts with default content for the
13941
- // slot which might have similar keys. Each vnode will always have a key that starts
13942
- // with a numeric character from compiler. In this case, we add a unique notation
13943
- // for slotted vnodes keys, e.g.: `@foo:1:1`. Note that this is *not* needed for
13944
- // dynamic keys, since `api.k` already scopes based on the iteration.
13945
- key = `@${slotParentName}:${key}`;
13946
- }
13947
- data.push(property$1(identifier('key'), literal$1(key)));
13948
- }
14134
+ data.push(property$1(identifier('key'), codeGen.genKeyExpression(forKey, slotParentName)));
13949
14135
  // Event handler
13950
14136
  if (listeners.length) {
13951
14137
  data.push(codeGen.genEventListeners(listeners));
@@ -14059,5 +14245,5 @@ function compile(source, config) {
14059
14245
  exports.compile = compile;
14060
14246
  exports.default = compile;
14061
14247
  exports.parse = parse;
14062
- /** version: 6.3.3 */
14248
+ /** version: 6.4.0 */
14063
14249
  //# sourceMappingURL=index.cjs.js.map