@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.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) 2024 Salesforce, Inc.
3
3
  */
4
4
  import { invariant, TemplateErrors, generateCompilerError, CompilerError, normalizeToDiagnostic, generateCompilerDiagnostic, ParserDiagnostics, DiagnosticLevel, CompilerMetrics } from '@lwc/errors';
5
- import { hasOwnProperty, getAPIVersionFromNumber, HTML_NAMESPACE, AriaAttrNameToPropNameMap, isAPIFeatureEnabled, isBooleanAttribute, isGlobalHtmlAttribute, ID_REFERENCING_ATTRIBUTES_SET, SVG_NAMESPACE, isAriaAttribute, isUndefined, isVoidElement, MATHML_NAMESPACE, isNull, htmlEscape, LWC_VERSION_COMMENT } from '@lwc/shared';
5
+ import { StringCharAt, hasOwnProperty, getAPIVersionFromNumber, HTML_NAMESPACE, AriaAttrNameToPropNameMap, isAPIFeatureEnabled, ID_REFERENCING_ATTRIBUTES_SET, SVG_NAMESPACE, isBooleanAttribute, isGlobalHtmlAttribute, isAriaAttribute, isUndefined, isVoidElement, MATHML_NAMESPACE, isNull, isArray, ArrayEvery, htmlEscape, LWC_VERSION_COMMENT } from '@lwc/shared';
6
6
  import * as he from 'he';
7
7
  import { walk } from 'estree-walker';
8
8
  import { parseExpressionAt, isIdentifierStart, isIdentifierChar } from 'acorn';
@@ -51,7 +51,10 @@ const DASHED_TAGNAME_ELEMENT_SET = new Set([
51
51
  'missing-glyph',
52
52
  ]);
53
53
  // Subset of LWC template directives that can safely be statically optimized
54
- const STATIC_SAFE_DIRECTIVES = new Set(['Ref']);
54
+ const STATIC_SAFE_DIRECTIVES = new Set([
55
+ 'Ref',
56
+ 'Key',
57
+ ]);
55
58
 
56
59
  /*
57
60
  * Copyright (c) 2024, Salesforce, Inc.
@@ -63,7 +66,7 @@ function toPropertyName(attr) {
63
66
  let prop = '';
64
67
  let shouldUpperCaseNext = false;
65
68
  for (let i = 0; i < attr.length; i++) {
66
- const char = attr.charAt(i);
69
+ const char = StringCharAt.call(attr, i);
67
70
  if (char === '-') {
68
71
  shouldUpperCaseNext = true;
69
72
  }
@@ -9293,6 +9296,9 @@ function isProperty(node) {
9293
9296
  function isScopedSlotFragment(node) {
9294
9297
  return node.type === 'ScopedSlotFragment';
9295
9298
  }
9299
+ function isAttribute$1(node) {
9300
+ return node.type === 'Attribute';
9301
+ }
9296
9302
 
9297
9303
  var LWCDirectiveDomMode;
9298
9304
  (function (LWCDirectiveDomMode) {
@@ -12578,6 +12584,18 @@ function parseClassNames(classNames) {
12578
12584
  .map((className) => className.trim())
12579
12585
  .filter((className) => className.length);
12580
12586
  }
12587
+
12588
+ /*
12589
+ * Copyright (c) 2024, salesforce.com, inc.
12590
+ * All rights reserved.
12591
+ * SPDX-License-Identifier: MIT
12592
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
12593
+ */
12594
+ // This set keeps track of static safe elements that have dynamic text in their direct children.
12595
+ const STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET = new WeakSet();
12596
+ // This map keeps track of static safe elements to their transformed children.
12597
+ // The children are transformed so that contiguous text nodes are consolidated into arrays.
12598
+ const STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE = new WeakMap();
12581
12599
  function isStaticNode(node, apiVersion) {
12582
12600
  let result = true;
12583
12601
  const { name: nodeName, namespace = '', attributes, directives, properties } = node;
@@ -12590,8 +12608,9 @@ function isStaticNode(node, apiVersion) {
12590
12608
  // it is an element
12591
12609
  result &&= isElement(node);
12592
12610
  // all attrs are static-safe
12611
+ // the criteria to determine safety can be found in computeAttrValue
12593
12612
  result &&= attributes.every(({ name, value }) => {
12594
- return (isLiteral(value) &&
12613
+ const isStaticSafeLiteral = isLiteral(value) &&
12595
12614
  name !== 'slot' &&
12596
12615
  // check for ScopedId
12597
12616
  name !== 'id' &&
@@ -12601,7 +12620,13 @@ function isStaticNode(node, apiVersion) {
12601
12620
  !isSvgUseHref(nodeName, name, namespace) &&
12602
12621
  // Check for ScopedFragId
12603
12622
  !(isAllowedFragOnlyUrlsXHTML(nodeName, name, namespace) &&
12604
- isFragmentOnlyUrl(value.value)));
12623
+ isFragmentOnlyUrl(value.value));
12624
+ const isStaticSafeExpression = isExpression$1(value) &&
12625
+ name !== 'slot' &&
12626
+ // TODO [#3624]: Revisit whether svgs can be included in static content optimization
12627
+ // svg href needs sanitization.
12628
+ !isSvgUseHref(nodeName, name, namespace);
12629
+ return isStaticSafeLiteral || isStaticSafeExpression;
12605
12630
  });
12606
12631
  // all directives are static-safe
12607
12632
  result &&= !directives.some((directive) => !STATIC_SAFE_DIRECTIVES.has(directive.name));
@@ -12610,30 +12635,40 @@ function isStaticNode(node, apiVersion) {
12610
12635
  return result;
12611
12636
  }
12612
12637
  function collectStaticNodes(node, staticNodes, state) {
12613
- let childrenAreStatic = true;
12614
- let nodeIsStatic;
12638
+ let childrenAreStaticSafe = true;
12639
+ let nodeIsStaticSafe;
12615
12640
  if (isText(node)) {
12616
- nodeIsStatic = isLiteral(node.value);
12641
+ nodeIsStaticSafe = true;
12617
12642
  }
12618
12643
  else if (isComment(node)) {
12619
- nodeIsStatic = true;
12644
+ nodeIsStaticSafe = true;
12620
12645
  }
12621
12646
  else {
12647
+ let hasDynamicText = false;
12622
12648
  // it is ElseBlock | ForBlock | If | BaseElement
12623
12649
  node.children.forEach((childNode) => {
12624
12650
  collectStaticNodes(childNode, staticNodes, state);
12625
- childrenAreStatic &&= staticNodes.has(childNode);
12651
+ childrenAreStaticSafe &&= staticNodes.has(childNode);
12652
+ // Collect nodes that have dynamic text ahead of time.
12653
+ // We only need to know if the direct child has dynamic text.
12654
+ hasDynamicText ||= isText(childNode) && !isStringLiteral(childNode.value);
12626
12655
  });
12627
12656
  // for IfBlock and ElseifBlock, traverse down the else branch
12628
12657
  if (isConditionalParentBlock(node) && node.else) {
12629
12658
  collectStaticNodes(node.else, staticNodes, state);
12630
12659
  }
12631
- nodeIsStatic =
12660
+ nodeIsStaticSafe =
12632
12661
  isBaseElement(node) &&
12633
12662
  !isCustomRendererHookRequired(node, state) &&
12634
12663
  isStaticNode(node, state.config.apiVersion);
12664
+ if (nodeIsStaticSafe && hasDynamicText) {
12665
+ // Track when the static element contains dynamic text.
12666
+ // This will alter the way the children need to be traversed to apply static parts.
12667
+ // See transformStaticChildren below.
12668
+ STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET.add(node);
12669
+ }
12635
12670
  }
12636
- if (nodeIsStatic && childrenAreStatic) {
12671
+ if (nodeIsStaticSafe && childrenAreStaticSafe) {
12637
12672
  staticNodes.add(node);
12638
12673
  }
12639
12674
  }
@@ -12644,6 +12679,62 @@ function getStaticNodes(root, state) {
12644
12679
  });
12645
12680
  return staticNodes;
12646
12681
  }
12682
+ // The purpose of this function is to concatenate the contiguous text nodes into a single array
12683
+ // to simplify the traversing logic when generating static parts and serializing the element.
12684
+ function transformStaticChildren(elm) {
12685
+ const children = elm.children;
12686
+ if (!children.length || !STATIC_ELEMENT_WITH_DYNAMIC_TEXT_SET.has(elm)) {
12687
+ // The element either has no children or its children does not contain dynamic text.
12688
+ return children;
12689
+ }
12690
+ if (STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.has(elm)) {
12691
+ return STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.get(elm);
12692
+ }
12693
+ const result = [];
12694
+ const len = children.length;
12695
+ let current;
12696
+ let next;
12697
+ let contiguousTextNodes = null;
12698
+ for (let i = 0; i < len; i++) {
12699
+ current = children[i];
12700
+ if (!isText(current)) {
12701
+ contiguousTextNodes = null;
12702
+ result.push(current);
12703
+ }
12704
+ else {
12705
+ if (!isNull(contiguousTextNodes)) {
12706
+ // Already in a contiguous text node chain
12707
+ // All contiguous nodes represent an expression in the source, it's guaranteed by the parser.
12708
+ contiguousTextNodes.push(current);
12709
+ }
12710
+ else {
12711
+ next = children[i + 1];
12712
+ if (isExpression$1(current) || (!isUndefined(next) && isText(next))) {
12713
+ // Text nodes can appear as follows:
12714
+ // 1. A single text literal node.
12715
+ // 2. A single text expression node.
12716
+ // 3. Contiguous series of text nodes (literal/expression mixed) with at least 1 expression.
12717
+ // When there is an expression in the source, the text nodes are split into contiguous text nodes.
12718
+ // When there is no expression in the source, the text will appear as a single text literal.
12719
+ // We normalize all of the contiguous text nodes or single text expression to an array.
12720
+ // Single text literal nodes (no expression or are not part of a contiguous set of text nodes) remain text nodes
12721
+ // and will not be consolidated to an array.
12722
+ // This is to normalize the traversal behavior when creating static parts and when serializing
12723
+ // the elements.
12724
+ contiguousTextNodes = [current];
12725
+ }
12726
+ // When contiguousTextNodes is null it is a single string literal.
12727
+ result.push(contiguousTextNodes ?? current);
12728
+ }
12729
+ }
12730
+ }
12731
+ STATIC_ELEMENT_TO_DYNAMIC_TEXT_CHILDREN_CACHE.set(elm, result);
12732
+ return result;
12733
+ }
12734
+ // Dynamic text is consolidated from individual text arrays into a single Text[].
12735
+ // Static text = a single text literal node (not in an array).
12736
+ // Dynamic text = At least 1 text expression node + 0 or more text literal nodes (always in an array).
12737
+ const isDynamicText = (nodes) => isArray(nodes) && ArrayEvery.call(nodes, isText);
12647
12738
 
12648
12739
  /*
12649
12740
  * Copyright (c) 2018, salesforce.com, inc.
@@ -12670,7 +12761,7 @@ const rawContentElements = new Set([
12670
12761
  function templateStringEscape(str) {
12671
12762
  return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
12672
12763
  }
12673
- function serializeAttrs(element) {
12764
+ function serializeAttrs(element, codeGen) {
12674
12765
  /**
12675
12766
  * 0: styleToken in existing class attr
12676
12767
  * 1: styleToken for added class attr
@@ -12678,24 +12769,36 @@ function serializeAttrs(element) {
12678
12769
  */
12679
12770
  const attrs = [];
12680
12771
  let hasClassAttr = false;
12681
- const collector = ({ name, value }) => {
12772
+ const collector = ({ name, value, hasExpression, }) => {
12682
12773
  let v = typeof value === 'string' ? templateStringEscape(value) : value;
12683
12774
  if (name === 'class') {
12684
12775
  hasClassAttr = true;
12685
- v += '${0}';
12776
+ // ${0} maps to class token that will be appended to the string.
12777
+ // See buildParseFragmentFn for details.
12778
+ // The token is only needed when the class attribute is static.
12779
+ // The token will be injected at runtime for expressions in parseFragmentFn.
12780
+ if (!hasExpression) {
12781
+ v += '${0}';
12782
+ }
12686
12783
  }
12687
12784
  if (typeof v === 'string') {
12688
- attrs.push(`${name}="${htmlEscape(v, true)}"`);
12785
+ // Inject a placeholder where the staticPartId will go when an expression occurs.
12786
+ // This is only needed for SSR to inject the expression value during serialization.
12787
+ attrs.push(hasExpression ? `\${"${v}"}` : ` ${name}="${htmlEscape(v, true)}"`);
12689
12788
  }
12690
12789
  else {
12691
- attrs.push(name);
12790
+ attrs.push(` ${name}`);
12692
12791
  }
12693
12792
  };
12694
12793
  element.attributes
12695
12794
  .map((attr) => {
12795
+ const hasExpression = isExpression$1(attr.value);
12696
12796
  return {
12797
+ hasExpression,
12697
12798
  name: attr.name,
12698
- value: attr.value.value,
12799
+ value: hasExpression
12800
+ ? codeGen.getStaticExpressionToken(attr)
12801
+ : attr.value.value,
12699
12802
  };
12700
12803
  })
12701
12804
  .forEach(collector);
@@ -12711,31 +12814,42 @@ function serializeAttrs(element) {
12711
12814
  };
12712
12815
  })
12713
12816
  .forEach(collector);
12714
- return (attrs.length > 0 ? ' ' : '') + attrs.join(' ') + (hasClassAttr ? '${2}' : '${3}');
12817
+ // ${2} maps to style token attribute
12818
+ // ${3} maps to class attribute token + style token attribute
12819
+ // See buildParseFragmentFn for details.
12820
+ return attrs.join('') + (hasClassAttr ? '${2}' : '${3}');
12715
12821
  }
12716
- function serializeChildren(children, parentTagName, preserveComments) {
12822
+ function serializeChildren(node, parentTagName, codeGen) {
12717
12823
  let html = '';
12718
- children.forEach((child) => {
12824
+ for (const child of transformStaticChildren(node)) {
12719
12825
  /* istanbul ignore else */
12720
- if (isElement(child)) {
12721
- html += serializeStaticElement(child, preserveComments);
12826
+ if (isDynamicText(child)) {
12827
+ html += serializeDynamicTextNode(child, codeGen);
12722
12828
  }
12723
12829
  else if (isText(child)) {
12724
- html += serializeTextNode(child, rawContentElements.has(parentTagName.toUpperCase()));
12830
+ html += serializeStaticTextNode(child, rawContentElements.has(parentTagName.toUpperCase()));
12831
+ }
12832
+ else if (isElement(child)) {
12833
+ html += serializeStaticElement(child, codeGen);
12725
12834
  }
12726
12835
  else if (isComment(child)) {
12727
- html += serializeCommentNode(child, preserveComments);
12836
+ html += serializeCommentNode(child, codeGen.preserveComments);
12728
12837
  }
12729
12838
  else {
12730
12839
  throw new TypeError('Unknown node found while serializing static content. Allowed nodes types are: Element, Text and Comment.');
12731
12840
  }
12732
- });
12841
+ }
12733
12842
  return html;
12734
12843
  }
12735
12844
  function serializeCommentNode(comment, preserveComment) {
12736
12845
  return preserveComment ? `<!--${htmlEscape(templateStringEscape(comment.value))}-->` : '';
12737
12846
  }
12738
- function serializeTextNode(text, useRawContent) {
12847
+ function serializeDynamicTextNode(textNodes, codeGen) {
12848
+ // The first text node is they key for contiguous text nodes and single expressions.
12849
+ // This is guaranteed to have a value by the isDynamicText check.
12850
+ return `\${"${codeGen.getStaticExpressionToken(textNodes[0])}"}`;
12851
+ }
12852
+ function serializeStaticTextNode(text, useRawContent) {
12739
12853
  let content;
12740
12854
  if (useRawContent) {
12741
12855
  content = text.raw;
@@ -12743,19 +12857,20 @@ function serializeTextNode(text, useRawContent) {
12743
12857
  else {
12744
12858
  content = htmlEscape(text.value.value);
12745
12859
  }
12746
- return templateStringEscape(content);
12860
+ content = templateStringEscape(content);
12861
+ return content;
12747
12862
  }
12748
- function serializeStaticElement(element, preserveComments) {
12863
+ function serializeStaticElement(element, codeGen) {
12749
12864
  const { name: tagName, namespace } = element;
12750
12865
  const isForeignElement = namespace !== HTML_NAMESPACE;
12751
12866
  const hasChildren = element.children.length > 0;
12752
- let html = `<${tagName}${serializeAttrs(element)}`;
12867
+ let html = `<${tagName}${serializeAttrs(element, codeGen)}`;
12753
12868
  if (isForeignElement && !hasChildren) {
12754
12869
  html += '/>';
12755
12870
  return html;
12756
12871
  }
12757
12872
  html += '>';
12758
- html += serializeChildren(element.children, tagName, preserveComments);
12873
+ html += serializeChildren(element, tagName, codeGen);
12759
12874
  if (!isVoidElement(tagName, namespace) || hasChildren) {
12760
12875
  html += `</${tagName}>`;
12761
12876
  }
@@ -12907,6 +13022,42 @@ function collectParamsFromMemberExpression(_node, _vars) {
12907
13022
  // the AST, we'll validate anyway.
12908
13023
  invariant(false, ParserDiagnostics.INVALID_EXPR_ARROW_FN_PARAM, ['member expressions']);
12909
13024
  }
13025
+ function bindAttributeExpression(attr, element, codeGen, addLegacySanitizationHook) {
13026
+ const { name: elmName, namespace = '' } = element;
13027
+ const { value: attrValue } = attr;
13028
+ // Evaluate properties based on their attribute name
13029
+ const attrName = isProperty(attr) ? attr.attributeName : attr.name;
13030
+ const isUsedAsAttribute = isAttribute(element, attrName);
13031
+ const expression = codeGen.bindExpression(attrValue);
13032
+ // TODO [#2012]: Normalize global boolean attrs values passed to custom elements as props
13033
+ if (isUsedAsAttribute && isBooleanAttribute(attrName, elmName)) {
13034
+ // We need to do some manipulation to allow the diffing algorithm add/remove the attribute
13035
+ // without handling special cases at runtime.
13036
+ return codeGen.genBooleanAttributeExpr(expression);
13037
+ }
13038
+ if (attrName === 'tabindex') {
13039
+ return codeGen.genTabIndex([expression]);
13040
+ }
13041
+ if (attrName === 'id' || isIdReferencingAttribute(attrName)) {
13042
+ return codeGen.genScopedId(expression);
13043
+ }
13044
+ if (codeGen.scopeFragmentId && isAllowedFragOnlyUrlsXHTML(elmName, attrName, namespace)) {
13045
+ return codeGen.genScopedFragId(expression);
13046
+ }
13047
+ if (isSvgUseHref(elmName, attrName, namespace)) {
13048
+ if (addLegacySanitizationHook) {
13049
+ codeGen.usedLwcApis.add('sanitizeAttribute');
13050
+ return callExpression(identifier('sanitizeAttribute'), [
13051
+ literal$1(elmName),
13052
+ literal$1(namespace),
13053
+ literal$1(attrName),
13054
+ codeGen.genScopedFragId(expression),
13055
+ ]);
13056
+ }
13057
+ return codeGen.genScopedFragId(expression);
13058
+ }
13059
+ return expression;
13060
+ }
12910
13061
 
12911
13062
  /*
12912
13063
  * Copyright (c) 2018, salesforce.com, inc.
@@ -12920,27 +13071,27 @@ const doStructuredClone = typeof structuredClone === 'function'
12920
13071
  ? structuredClone
12921
13072
  : (obj) => JSON.parse(JSON.stringify(obj));
12922
13073
  const RENDER_APIS = {
12923
- iterator: { name: 'i', alias: 'api_iterator' },
12924
- flatten: { name: 'f', alias: 'api_flatten' },
12925
- element: { name: 'h', alias: 'api_element' },
12926
- slot: { name: 's', alias: 'api_slot' },
13074
+ bind: { name: 'b', alias: 'api_bind' },
13075
+ comment: { name: 'co', alias: 'api_comment' },
12927
13076
  customElement: { name: 'c', alias: 'api_custom_element' },
12928
- dynamicCtor: { name: 'dc', alias: 'api_dynamic_component' },
12929
13077
  // TODO [#3331]: remove usage of lwc:dynamic in 246
12930
13078
  deprecatedDynamicCtor: { name: 'ddc', alias: 'api_deprecated_dynamic_component' },
12931
- bind: { name: 'b', alias: 'api_bind' },
12932
- text: { name: 't', alias: 'api_text' },
13079
+ dynamicCtor: { name: 'dc', alias: 'api_dynamic_component' },
12933
13080
  dynamicText: { name: 'd', alias: 'api_dynamic_text' },
13081
+ element: { name: 'h', alias: 'api_element' },
13082
+ flatten: { name: 'f', alias: 'api_flatten' },
13083
+ fragment: { name: 'fr', alias: 'api_fragment' },
13084
+ iterator: { name: 'i', alias: 'api_iterator' },
12934
13085
  key: { name: 'k', alias: 'api_key' },
12935
- tabindex: { name: 'ti', alias: 'api_tab_index' },
12936
- scopedId: { name: 'gid', alias: 'api_scoped_id' },
12937
- scopedFragId: { name: 'fid', alias: 'api_scoped_frag_id' },
12938
- comment: { name: 'co', alias: 'api_comment' },
12939
13086
  sanitizeHtmlContent: { name: 'shc', alias: 'api_sanitize_html_content' },
12940
- fragment: { name: 'fr', alias: 'api_fragment' },
12941
- staticFragment: { name: 'st', alias: 'api_static_fragment' },
13087
+ scopedFragId: { name: 'fid', alias: 'api_scoped_frag_id' },
13088
+ scopedId: { name: 'gid', alias: 'api_scoped_id' },
12942
13089
  scopedSlotFactory: { name: 'ssf', alias: 'api_scoped_slot_factory' },
13090
+ slot: { name: 's', alias: 'api_slot' },
13091
+ staticFragment: { name: 'st', alias: 'api_static_fragment' },
12943
13092
  staticPart: { name: 'sp', alias: 'api_static_part' },
13093
+ tabindex: { name: 'ti', alias: 'api_tab_index' },
13094
+ text: { name: 't', alias: 'api_text' },
12944
13095
  };
12945
13096
  class CodeGen {
12946
13097
  constructor({ root, state, scopeFragmentId, }) {
@@ -12956,6 +13107,7 @@ class CodeGen {
12956
13107
  this.slotNames = new Set();
12957
13108
  this.memorizedIds = [];
12958
13109
  this.referencedComponents = new Set();
13110
+ this.staticExpressionMap = new WeakMap();
12959
13111
  this.root = root;
12960
13112
  if (state.config.enableStaticContentOptimization) {
12961
13113
  this.staticNodes = getStaticNodes(root, state);
@@ -13004,6 +13156,9 @@ class CodeGen {
13004
13156
  return this._renderApiCall(RENDER_APIS.deprecatedDynamicCtor, args);
13005
13157
  }
13006
13158
  genText(value) {
13159
+ return this._renderApiCall(RENDER_APIS.text, [this.genConcatenatedText(value)]);
13160
+ }
13161
+ genConcatenatedText(value) {
13007
13162
  const mappedValues = value.map((v) => {
13008
13163
  return typeof v === 'string'
13009
13164
  ? literal$1(v)
@@ -13013,7 +13168,7 @@ class CodeGen {
13013
13168
  for (let i = 1, n = mappedValues.length; i < n; i++) {
13014
13169
  textConcatenation = binaryExpression('+', textConcatenation, mappedValues[i]);
13015
13170
  }
13016
- return this._renderApiCall(RENDER_APIS.text, [textConcatenation]);
13171
+ return textConcatenation;
13017
13172
  }
13018
13173
  genComment(value) {
13019
13174
  return this._renderApiCall(RENDER_APIS.comment, [literal$1(value)]);
@@ -13034,9 +13189,6 @@ class CodeGen {
13034
13189
  genFlatten(children) {
13035
13190
  return this._renderApiCall(RENDER_APIS.flatten, children);
13036
13191
  }
13037
- genKey(compilerKey, value) {
13038
- return this._renderApiCall(RENDER_APIS.key, [compilerKey, value]);
13039
- }
13040
13192
  genScopedId(id) {
13041
13193
  if (typeof id === 'string') {
13042
13194
  return this._renderApiCall(RENDER_APIS.scopedId, [literal$1(id)]);
@@ -13097,6 +13249,28 @@ class CodeGen {
13097
13249
  this.hasRefs = true;
13098
13250
  return property$1(identifier('ref'), ref.value);
13099
13251
  }
13252
+ genKeyExpression(ref, slotParentName) {
13253
+ if (ref) {
13254
+ // If element has user-supplied `key` or is in iterator, call `api.k`
13255
+ const forKeyExpression = this.bindExpression(ref.value);
13256
+ const key = this.generateKey();
13257
+ return this._renderApiCall(RENDER_APIS.key, [literal$1(key), forKeyExpression]);
13258
+ }
13259
+ else {
13260
+ // If standalone element with no user-defined key
13261
+ let key = this.generateKey();
13262
+ // Parent slot name could be the empty string
13263
+ if (slotParentName !== undefined) {
13264
+ // Prefixing the key is necessary to avoid conflicts with default content for the
13265
+ // slot which might have similar keys. Each vnode will always have a key that starts
13266
+ // with a numeric character from compiler. In this case, we add a unique notation
13267
+ // for slotted vnodes keys, e.g.: `@foo:1:1`. Note that this is *not* needed for
13268
+ // dynamic keys, since `api.k` already scopes based on the iteration.
13269
+ key = `@${slotParentName}:${key}`;
13270
+ }
13271
+ return literal$1(key);
13272
+ }
13273
+ }
13100
13274
  /**
13101
13275
  * This routine generates an expression that avoids
13102
13276
  * computing the sanitized html of a raw html if it does not change
@@ -13206,10 +13380,9 @@ class CodeGen {
13206
13380
  return expression;
13207
13381
  }
13208
13382
  genStaticElement(element, slotParentName) {
13209
- const key = slotParentName !== undefined
13210
- ? `@${slotParentName}:${this.generateKey()}`
13211
- : this.generateKey();
13212
- const html = serializeStaticElement(element, this.preserveComments);
13383
+ const staticParts = this.genStaticParts(element);
13384
+ // Generate static parts prior to serialization to inject the corresponding static part Id into the serialized output.
13385
+ const html = serializeStaticElement(element, this);
13213
13386
  const parseMethod = element.name !== 'svg' && element.namespace === SVG_NAMESPACE
13214
13387
  ? PARSE_SVG_FRAGMENT_METHOD_NAME
13215
13388
  : PARSE_FRAGMENT_METHOD_NAME;
@@ -13230,9 +13403,14 @@ class CodeGen {
13230
13403
  identifier: identifier$1,
13231
13404
  expr,
13232
13405
  });
13233
- const args = [callExpression(identifier$1, []), literal$1(key)];
13406
+ // Keys are only supported at the top level of a static block, and are serialized directly in the args for
13407
+ // the `api_static_fragment` call. We don't need to support keys in static parts (i.e. children of
13408
+ // the top-level element), because the compiler ignores any keys that aren't direct children of a
13409
+ // for:each block (see error code 1149 - "KEY_SHOULD_BE_IN_ITERATION").
13410
+ const key = element.directives.find(isKeyDirective);
13411
+ const keyExpression = this.genKeyExpression(key, slotParentName);
13412
+ const args = [identifier$1, keyExpression];
13234
13413
  // Only add the third argument (staticParts) if this element needs it
13235
- const staticParts = this.genStaticParts(element);
13236
13414
  if (staticParts) {
13237
13415
  args.push(staticParts);
13238
13416
  }
@@ -13240,52 +13418,106 @@ class CodeGen {
13240
13418
  }
13241
13419
  genStaticParts(element) {
13242
13420
  const stack = [element];
13243
- const partIdsToDatabagProps = new Map();
13421
+ const partIdsToArgs = new Map();
13244
13422
  let partId = -1;
13245
- const addDatabagProp = (prop) => {
13246
- let databags = partIdsToDatabagProps.get(partId);
13247
- if (!databags) {
13248
- databags = [];
13249
- partIdsToDatabagProps.set(partId, databags);
13423
+ const getPartIdArgs = (partId) => {
13424
+ let args = partIdsToArgs.get(partId);
13425
+ if (!args) {
13426
+ args = { text: literal$1(null), databag: literal$1(null) };
13427
+ partIdsToArgs.set(partId, args);
13250
13428
  }
13251
- databags.push(prop);
13429
+ return args;
13430
+ };
13431
+ const setPartIdText = (text) => {
13432
+ const args = getPartIdArgs(partId);
13433
+ args.text = text;
13434
+ };
13435
+ const setPartIdDatabag = (databag) => {
13436
+ const args = getPartIdArgs(partId);
13437
+ args.databag = objectExpression(databag);
13252
13438
  };
13253
13439
  // Depth-first traversal. We assign a partId to each element, which is an integer based on traversal order.
13254
13440
  while (stack.length > 0) {
13255
- const node = stack.shift();
13441
+ const current = stack.shift();
13256
13442
  // Skip comment nodes in parts count, as they will be stripped in production, unless when `lwc:preserve-comments` is enabled
13257
- if (!isComment(node) || this.preserveComments) {
13443
+ if (isDynamicText(current) || !isComment(current) || this.preserveComments) {
13258
13444
  partId++;
13259
13445
  }
13260
- if (isElement(node)) {
13446
+ if (isDynamicText(current)) {
13447
+ const textNodes = current;
13448
+ const partToken = `${"t" /* STATIC_PART_TOKEN_ID.TEXT */}${partId}`;
13449
+ // Use the first text node as the key.
13450
+ // Dynamic text is guaranteed to have at least 1 text node in the array by transformStaticChildren.
13451
+ this.staticExpressionMap.set(textNodes[0], partToken);
13452
+ const concatenatedText = this.genConcatenatedText(textNodes.map(({ value }) => isStringLiteral(value) ? value.value : this.bindExpression(value)));
13453
+ setPartIdText(concatenatedText);
13454
+ }
13455
+ else if (isElement(current)) {
13456
+ const elm = current;
13457
+ const databag = [];
13261
13458
  // has event listeners
13262
- if (node.listeners.length) {
13263
- addDatabagProp(this.genEventListeners(node.listeners));
13459
+ if (elm.listeners.length) {
13460
+ databag.push(this.genEventListeners(elm.listeners));
13264
13461
  }
13265
- // see STATIC_SAFE_DIRECTIVES for what's allowed here
13266
- for (const directive of node.directives) {
13462
+ // See STATIC_SAFE_DIRECTIVES for what's allowed here.
13463
+ // Also note that we don't generate the 'key' here, because we only support it at the top level
13464
+ // directly passed into the `api_static_fragment` function, not as a part.
13465
+ for (const directive of elm.directives) {
13267
13466
  if (directive.name === 'Ref') {
13268
- addDatabagProp(this.genRef(directive));
13467
+ databag.push(this.genRef(directive));
13269
13468
  }
13270
13469
  }
13271
- // For depth-first traversal, children must be preprended in order, so that they are processed before
13470
+ const attributeExpressions = [];
13471
+ for (const attribute of elm.attributes) {
13472
+ const { name, value } = attribute;
13473
+ if (isExpression$1(value)) {
13474
+ let partToken = '';
13475
+ if (name === 'style') {
13476
+ partToken = `${"s" /* STATIC_PART_TOKEN_ID.STYLE */}${partId}`;
13477
+ databag.push(property$1(identifier('style'), this.bindExpression(value)));
13478
+ }
13479
+ else if (name === 'class') {
13480
+ partToken = `${"c" /* STATIC_PART_TOKEN_ID.CLASS */}${partId}`;
13481
+ databag.push(property$1(identifier('className'), this.bindExpression(value)));
13482
+ }
13483
+ else {
13484
+ partToken = `${"a" /* STATIC_PART_TOKEN_ID.ATTRIBUTE */}${partId}:${name}`;
13485
+ attributeExpressions.push(property$1(literal$1(name), bindAttributeExpression(attribute, elm, this, false)));
13486
+ }
13487
+ this.staticExpressionMap.set(attribute, partToken);
13488
+ }
13489
+ }
13490
+ if (attributeExpressions.length) {
13491
+ databag.push(property$1(identifier('attrs'), objectExpression(attributeExpressions)));
13492
+ }
13493
+ if (databag.length) {
13494
+ setPartIdDatabag(databag);
13495
+ }
13496
+ // For depth-first traversal, children must be prepended in order, so that they are processed before
13272
13497
  // siblings. Note that this is consistent with the order used in the diffing algo as well as
13273
13498
  // `traverseAndSetElements` in @lwc/engine-core.
13274
- stack.unshift(...node.children);
13499
+ stack.unshift(...transformStaticChildren(elm));
13275
13500
  }
13276
13501
  }
13277
- if (partIdsToDatabagProps.size === 0) {
13278
- return undefined; // no databags needed
13502
+ if (partIdsToArgs.size === 0) {
13503
+ return undefined; // no parts needed
13279
13504
  }
13280
- return arrayExpression([...partIdsToDatabagProps.entries()].map(([partId, databagProperties]) => {
13281
- return this.genStaticPart(partId, databagProperties);
13505
+ return arrayExpression([...partIdsToArgs.entries()].map(([partId, { databag, text }]) => {
13506
+ return this.genStaticPart(partId, databag, text);
13282
13507
  }));
13283
13508
  }
13284
- genStaticPart(partId, databagProperties) {
13285
- return this._renderApiCall(RENDER_APIS.staticPart, [
13286
- literal$1(partId),
13287
- objectExpression(databagProperties),
13288
- ]);
13509
+ genStaticPart(partId, data, text) {
13510
+ return this._renderApiCall(RENDER_APIS.staticPart, [literal$1(partId), data, text]);
13511
+ }
13512
+ getStaticExpressionToken(node) {
13513
+ const token = this.staticExpressionMap.get(node);
13514
+ /* istanbul ignore if */
13515
+ if (isUndefined(token)) {
13516
+ // It should not be possible to hit this code path
13517
+ const nodeName = isAttribute$1(node) ? node.name : 'text node';
13518
+ throw new Error(`Template compiler internal error, unable to map ${nodeName} to a static expression.`);
13519
+ }
13520
+ return token;
13289
13521
  }
13290
13522
  }
13291
13523
 
@@ -13476,13 +13708,15 @@ function format(templateFn, codeGen) {
13476
13708
  function transform(codeGen) {
13477
13709
  const instrumentation = codeGen.state.config.instrumentation;
13478
13710
  function transformElement(element, slotParentName) {
13711
+ // TODO [#4077]: Move databag gathering to after static element check as it doesn't seem to be used by static
13712
+ // content optimization.
13479
13713
  const databag = elementDataBag(element, slotParentName);
13480
- let res;
13481
13714
  if (codeGen.staticNodes.has(element) && isElement(element)) {
13482
13715
  // do not process children of static nodes.
13483
13716
  return codeGen.genStaticElement(element, slotParentName);
13484
13717
  }
13485
13718
  const children = transformChildren(element);
13719
+ let res;
13486
13720
  const { name } = element;
13487
13721
  // lwc:dynamic directive
13488
13722
  const deprecatedDynamicDirective = element.directives.find(isDynamicDirective);
@@ -13722,36 +13956,7 @@ function transform(codeGen) {
13722
13956
  const attrName = isProperty(attr) ? attr.attributeName : attr.name;
13723
13957
  const isUsedAsAttribute = isAttribute(element, attrName);
13724
13958
  if (isExpression$1(attrValue)) {
13725
- const expression = codeGen.bindExpression(attrValue);
13726
- // TODO [#2012]: Normalize global boolean attrs values passed to custom elements as props
13727
- if (isUsedAsAttribute && isBooleanAttribute(attrName, elmName)) {
13728
- // We need to do some manipulation to allow the diffing algorithm add/remove the attribute
13729
- // without handling special cases at runtime.
13730
- return codeGen.genBooleanAttributeExpr(expression);
13731
- }
13732
- if (attrName === 'tabindex') {
13733
- return codeGen.genTabIndex([expression]);
13734
- }
13735
- if (attrName === 'id' || isIdReferencingAttribute(attrName)) {
13736
- return codeGen.genScopedId(expression);
13737
- }
13738
- if (codeGen.scopeFragmentId &&
13739
- isAllowedFragOnlyUrlsXHTML(elmName, attrName, namespace)) {
13740
- return codeGen.genScopedFragId(expression);
13741
- }
13742
- if (isSvgUseHref(elmName, attrName, namespace)) {
13743
- if (addLegacySanitizationHook) {
13744
- codeGen.usedLwcApis.add('sanitizeAttribute');
13745
- return callExpression(identifier('sanitizeAttribute'), [
13746
- literal$1(elmName),
13747
- literal$1(namespace),
13748
- literal$1(attrName),
13749
- codeGen.genScopedFragId(expression),
13750
- ]);
13751
- }
13752
- return codeGen.genScopedFragId(expression);
13753
- }
13754
- return expression;
13959
+ return bindAttributeExpression(attr, element, codeGen, addLegacySanitizationHook);
13755
13960
  }
13756
13961
  else if (isStringLiteral(attrValue)) {
13757
13962
  if (attrName === 'id') {
@@ -13902,26 +14107,7 @@ function transform(codeGen) {
13902
14107
  data.push(property$1(identifier('context'), contextObj));
13903
14108
  }
13904
14109
  // Key property on VNode
13905
- if (forKey) {
13906
- // If element has user-supplied `key` or is in iterator, call `api.k`
13907
- const forKeyExpression = codeGen.bindExpression(forKey.value);
13908
- const generatedKey = codeGen.genKey(literal$1(codeGen.generateKey()), forKeyExpression);
13909
- data.push(property$1(identifier('key'), generatedKey));
13910
- }
13911
- else {
13912
- // If standalone element with no user-defined key
13913
- let key = codeGen.generateKey();
13914
- // Parent slot name could be the empty string
13915
- if (slotParentName !== undefined) {
13916
- // Prefixing the key is necessary to avoid conflicts with default content for the
13917
- // slot which might have similar keys. Each vnode will always have a key that starts
13918
- // with a numeric character from compiler. In this case, we add a unique notation
13919
- // for slotted vnodes keys, e.g.: `@foo:1:1`. Note that this is *not* needed for
13920
- // dynamic keys, since `api.k` already scopes based on the iteration.
13921
- key = `@${slotParentName}:${key}`;
13922
- }
13923
- data.push(property$1(identifier('key'), literal$1(key)));
13924
- }
14110
+ data.push(property$1(identifier('key'), codeGen.genKeyExpression(forKey, slotParentName)));
13925
14111
  // Event handler
13926
14112
  if (listeners.length) {
13927
14113
  data.push(codeGen.genEventListeners(listeners));
@@ -14033,5 +14219,5 @@ function compile(source, config) {
14033
14219
  }
14034
14220
 
14035
14221
  export { ElementDirectiveName, LWCDirectiveDomMode, LWCDirectiveRenderMode, LwcTagName, RootDirectiveName, TemplateDirectiveName, compile, compile as default, parse };
14036
- /** version: 6.3.3 */
14222
+ /** version: 6.4.0 */
14037
14223
  //# sourceMappingURL=index.js.map