@lwc/template-compiler 8.5.0 → 8.7.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
@@ -11390,6 +11390,8 @@ function isAttribute(element, attrName) {
11390
11390
  }
11391
11391
  // Handle input tag value="" and checked attributes that are only used for state initialization.
11392
11392
  // Because .setAttribute() won't update the value, those attributes should be considered as props.
11393
+ // Note: this is tightly-coupled with static-element-serializer.ts which treats `<input checked="...">`
11394
+ // and `<input value="...">` as special because of the logic below.
11393
11395
  if (element.name === 'input' && (attrName === 'value' || attrName === 'checked')) {
11394
11396
  return false;
11395
11397
  }
@@ -11563,6 +11565,7 @@ function parseElement(ctx, parse5Elm, parentNode, parse5ParentLocation) {
11563
11565
  applyKey(ctx, parsedAttr, element);
11564
11566
  applyLwcDirectives(ctx, parsedAttr, element);
11565
11567
  applyAttributes(ctx, parsedAttr, element);
11568
+ validateSlotAttribute(ctx, parsedAttr, parentNode, element);
11566
11569
  validateElement(ctx, element, parse5Elm);
11567
11570
  validateAttributes(ctx, parsedAttr, element);
11568
11571
  validateProperties(ctx, element);
@@ -12565,6 +12568,31 @@ function validateAttributes(ctx, parsedAttr, element) {
12565
12568
  }
12566
12569
  }
12567
12570
  }
12571
+ function validateSlotAttribute(ctx, parsedAttr, parentNode, element) {
12572
+ const slotAttr = parsedAttr.get('slot');
12573
+ if (!slotAttr) {
12574
+ return;
12575
+ }
12576
+ function isElementOrSlot(node) {
12577
+ return isElement(node) || isSlot(node);
12578
+ }
12579
+ // Find the nearest ancestor that is an element or `<slot>`, and stop if we hit a component.
12580
+ // E.g. this should warn due to the `<div>`: `<x-foo><div><span slot=bar></span></div></x-foo>`
12581
+ // And this should _not_ warn: `<div><x-foo><span slot=bar></span></x-foo></div>`
12582
+ const elementOrSlotAncestor = ctx.findAncestor(isElementOrSlot, ({ current }) => current && !isComponent(current) && !isExternalComponent(current), parentNode);
12583
+ // Warn if a `slot` attribute is on an element that isn't an immediate child of a containing LWC component or
12584
+ // `lwc:external` component. This is a case that all three of native-shadow/synthetic-shadow/light DOM will
12585
+ // simply ignore, but it's good to warn, so that developers realize that they may be making a mistake.
12586
+ // Note that, for the purposes of being considered an "immediate child," virtual elements like `for:each` and
12587
+ // `lwc:if` don't count - only rendered elements (including `<slot>`s) count.
12588
+ // Example of invalid usage: `<x-foo><div><span slot=bar></span></div></x-foo>`
12589
+ if (elementOrSlotAncestor) {
12590
+ ctx.warnOnNode(errors.ParserDiagnostics.IGNORED_SLOT_ATTRIBUTE_IN_CHILD, slotAttr, [
12591
+ `<${element.name}>`,
12592
+ `<${elementOrSlotAncestor.name}>`,
12593
+ ]);
12594
+ }
12595
+ }
12568
12596
  function validateProperties(ctx, element) {
12569
12597
  for (const prop of element.properties) {
12570
12598
  const { attributeName: attrName, value } = prop;
@@ -12874,8 +12902,30 @@ function isStaticNode(node, apiVersion) {
12874
12902
  });
12875
12903
  // all directives are static-safe
12876
12904
  result &&= !directives.some((directive) => !STATIC_SAFE_DIRECTIVES.has(directive.name));
12877
- // all properties are static
12878
- result &&= properties.every((prop) => isLiteral(prop.value));
12905
+ // Sanity check to ensure that only `<input value>`/`<input checked>` are treated as props for elements
12906
+ /* v8 ignore start */
12907
+ if (process.env.NODE_ENV === 'test' && isElement(node)) {
12908
+ for (const { attributeName } of properties) {
12909
+ if (node.name !== 'input' &&
12910
+ !(attributeName === 'checked' || attributeName === 'value')) {
12911
+ throw new Error(`Expected to only see <input value>/<input checked> treated as an element prop. ` +
12912
+ `Instead found <${node.name} ${attributeName}>`);
12913
+ }
12914
+ }
12915
+ }
12916
+ /* v8 ignore stop */
12917
+ // `<input checked="...">` and `<input value="...">` have a peculiar attr/prop relationship, so the engine
12918
+ // has historically treated them as props rather than attributes:
12919
+ // https://github.com/salesforce/lwc/blob/b584d39/packages/%40lwc/template-compiler/src/parser/attribute.ts#L217-L221
12920
+ // For example, an element might be rendered as `<input type=checkbox>` but `input.checked` could
12921
+ // still return true. `value` behaves similarly. `value` and `checked` behave surprisingly
12922
+ // because the attributes actually represent the "default" value rather than the current one:
12923
+ // - https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields
12924
+ // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#checked
12925
+ // For this reason, we currently avoid the static content optimization, and treat `value`/`checked` only as
12926
+ // runtime props.
12927
+ // TODO [#4775]: allow static optimization for `<input value>`/`<input checked>`
12928
+ result &&= properties.length === 0;
12879
12929
  return result;
12880
12930
  }
12881
12931
  function collectStaticNodes(node, staticNodes, state) {
@@ -13055,11 +13105,23 @@ function serializeAttrs(element, codeGen) {
13055
13105
  // Note that, to maintain backwards compatibility with the non-static output, we treat the valueless
13056
13106
  // "boolean" format (e.g. `<div id>`) as the empty string, which is semantically equivalent.
13057
13107
  const needsPlaceholder = hasExpression || hasSvgUseHref || needsScoping;
13058
- // Inject a placeholder where the staticPartId will go when an expression occurs.
13059
- // This is only needed for SSR to inject the expression value during serialization.
13060
- attrs.push(needsPlaceholder
13061
- ? `\${"${v}"}`
13062
- : ` ${escapedAttributeName}="${shared.htmlEscape(v, true)}"`);
13108
+ let nameAndValue;
13109
+ if (needsPlaceholder) {
13110
+ // Inject a placeholder where the staticPartId will go when an expression occurs.
13111
+ // This is only needed for SSR to inject the expression value during serialization.
13112
+ nameAndValue = `\${"${v}"}`;
13113
+ }
13114
+ else if (v === '') {
13115
+ // In HTML, there is no difference between the empty string value (`<div foo="">`) and "boolean true"
13116
+ // (`<div foo>`). They are both parsed identically, and the DOM treats them the same (`getAttribute`
13117
+ // returns the empty string). Here we prefer the shorter format.
13118
+ // https://html.spec.whatwg.org/multipage/introduction.html#a-quick-introduction-to-html:syntax-attributes
13119
+ nameAndValue = ` ${escapedAttributeName}`;
13120
+ }
13121
+ else {
13122
+ nameAndValue = ` ${escapedAttributeName}="${shared.htmlEscape(v, true)}"`;
13123
+ }
13124
+ attrs.push(nameAndValue);
13063
13125
  }
13064
13126
  else {
13065
13127
  attrs.push(` ${escapedAttributeName}`);
@@ -13091,21 +13153,16 @@ function serializeAttrs(element, codeGen) {
13091
13153
  value: hasExpression || hasSvgUseHref || needsScoping
13092
13154
  ? codeGen.getStaticExpressionToken(attr)
13093
13155
  : value.value,
13156
+ elementName: element.name,
13094
13157
  };
13095
13158
  })
13096
13159
  .forEach(collector);
13097
- // This is tightly coupled with the logic in the parser that decides when an attribute should be
13098
- // a property: https://github.com/salesforce/lwc/blob/master/packages/%40lwc/template-compiler/src/parser/attribute.ts#L198-L218
13099
- // Because a component can't be a static element, we only look in the property bag on value and checked attribute
13100
- // from the input.
13101
- element.properties
13102
- .map((prop) => {
13103
- return {
13104
- name: prop.attributeName,
13105
- value: prop.value.value,
13106
- };
13107
- })
13108
- .forEach(collector);
13160
+ /* v8 ignore start */
13161
+ // TODO [#4775]: allow static optimization for `<input value>`/`<input checked>`
13162
+ if (process.env.NODE_ENV === 'test' && element.properties.length > 0) {
13163
+ throw new Error('Expected zero properties at this point, found ' + element.properties.length);
13164
+ }
13165
+ /* v8 ignore stop */
13109
13166
  // ${2} maps to style token attribute
13110
13167
  // ${3} maps to class attribute token + style token attribute
13111
13168
  // See buildParseFragmentFn for details.
@@ -14155,17 +14212,29 @@ function transform(codeGen) {
14155
14212
  const children = parent.children;
14156
14213
  const childrenIterator = children[Symbol.iterator]();
14157
14214
  let current;
14215
+ function isTextOrIgnoredComment(node) {
14216
+ return isText(node) || (isComment(node) && !codeGen.preserveComments);
14217
+ }
14158
14218
  while ((current = childrenIterator.next()) && !current.done) {
14159
14219
  let child = current.value;
14160
- if (isText(child)) {
14220
+ // Concatenate contiguous text nodes together (while skipping ignored comments)
14221
+ // E.g. `<div>{foo}{bar}</div>` can be concatenated into a single text node expression,
14222
+ // and so can `<div>{foo}<!-- baz -->{bar}</div>` if comments are ignored.
14223
+ if (isTextOrIgnoredComment(child)) {
14161
14224
  const continuousText = [];
14162
14225
  // Consume all the contiguous text nodes.
14163
14226
  do {
14164
- continuousText.push(child);
14227
+ if (isText(child)) {
14228
+ continuousText.push(child);
14229
+ }
14165
14230
  current = childrenIterator.next();
14166
14231
  child = current.value;
14167
- } while (!current.done && isText(child));
14168
- res.push(transformText(continuousText));
14232
+ } while (!current.done && isTextOrIgnoredComment(child));
14233
+ // Only push an api_text call if we actually have text to render.
14234
+ // (We might just have iterated through a sequence of ignored comments.)
14235
+ if (continuousText.length) {
14236
+ res.push(transformText(continuousText));
14237
+ }
14169
14238
  // Early exit if a text node is the last child node.
14170
14239
  if (current.done) {
14171
14240
  break;
@@ -14636,5 +14705,5 @@ exports.generateScopeTokens = generateScopeTokens;
14636
14705
  exports.kebabcaseToCamelcase = kebabcaseToCamelcase;
14637
14706
  exports.parse = parse;
14638
14707
  exports.toPropertyName = toPropertyName;
14639
- /** version: 8.5.0 */
14708
+ /** version: 8.7.0 */
14640
14709
  //# sourceMappingURL=index.cjs.js.map