@lwc/template-compiler 8.5.0 → 8.6.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;
@@ -13011,14 +13039,33 @@ function serializeAttrs(element, codeGen) {
13011
13039
  */
13012
13040
  const attrs = [];
13013
13041
  let hasClassAttr = false;
13014
- const collector = ({ name, value, hasExpression, hasSvgUseHref, needsScoping, }) => {
13042
+ const collector = ({ name, value, isProp, hasExpression, hasSvgUseHref, needsScoping, }) => {
13015
13043
  // Do not serialize boolean class/style attribute (consistent with non-static optimized)
13016
13044
  if (typeof value === 'boolean' && (name === 'class' || name === 'style')) {
13017
13045
  return;
13018
13046
  }
13019
13047
  // See W-16614169
13020
13048
  const escapedAttributeName = templateStringEscape(name);
13021
- if (typeof value === 'string') {
13049
+ // `<input checked="...">` and `<input value="...">` have a peculiar attr/prop relationship, so the engine
13050
+ // has historically treated them as props rather than attributes:
13051
+ // https://github.com/salesforce/lwc/blob/b584d39/packages/%40lwc/template-compiler/src/parser/attribute.ts#L217-L221
13052
+ // For example, an element might be rendered as `<input type=checkbox>` but `input.checked` could
13053
+ // still return true. `value` behaves similarly. `value` and `checked` behave surprisingly
13054
+ // because the attributes actually represent the "default" value rather than the current one:
13055
+ // - https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields
13056
+ // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#checked
13057
+ if (isProp) {
13058
+ // The below logic matches the behavior of non-static-optimized DOM nodes.
13059
+ // There is no functional difference between e.g. `checked="checked"` and `checked` but we
13060
+ // match byte-for-byte the non-static-optimized HTML that would be rendered.
13061
+ if (name === 'checked' || (name === 'value' && value === '')) {
13062
+ attrs.push(` ${escapedAttributeName}`);
13063
+ }
13064
+ else {
13065
+ attrs.push(` ${escapedAttributeName}="${shared.htmlEscape(String(value), true)}"`);
13066
+ }
13067
+ }
13068
+ else if (typeof value === 'string') {
13022
13069
  let v = templateStringEscape(value);
13023
13070
  if (name === 'class') {
13024
13071
  // ${0} maps to class token that will be appended to the string.
@@ -13054,12 +13101,25 @@ function serializeAttrs(element, codeGen) {
13054
13101
  // Skip serializing here and handle it as if it were a dynamic attribute instead.
13055
13102
  // Note that, to maintain backwards compatibility with the non-static output, we treat the valueless
13056
13103
  // "boolean" format (e.g. `<div id>`) as the empty string, which is semantically equivalent.
13104
+ // `isProp` corresponds to `value` or `checked` on an `<input>` which is treated as a prop at runtime.
13057
13105
  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)}"`);
13106
+ let nameAndValue;
13107
+ if (needsPlaceholder) {
13108
+ // Inject a placeholder where the staticPartId will go when an expression occurs.
13109
+ // This is only needed for SSR to inject the expression value during serialization.
13110
+ nameAndValue = `\${"${v}"}`;
13111
+ }
13112
+ else if (v === '') {
13113
+ // In HTML, there is no difference between the empty string value (`<div foo="">`) and "boolean true"
13114
+ // (`<div foo>`). They are both parsed identically, and the DOM treats them the same (`getAttribute`
13115
+ // returns the empty string). Here we prefer the shorter format.
13116
+ // https://html.spec.whatwg.org/multipage/introduction.html#a-quick-introduction-to-html:syntax-attributes
13117
+ nameAndValue = ` ${escapedAttributeName}`;
13118
+ }
13119
+ else {
13120
+ nameAndValue = ` ${escapedAttributeName}="${shared.htmlEscape(v, true)}"`;
13121
+ }
13122
+ attrs.push(nameAndValue);
13063
13123
  }
13064
13124
  else {
13065
13125
  attrs.push(` ${escapedAttributeName}`);
@@ -13086,23 +13146,33 @@ function serializeAttrs(element, codeGen) {
13086
13146
  return {
13087
13147
  hasExpression,
13088
13148
  hasSvgUseHref,
13149
+ isProp: false,
13089
13150
  needsScoping,
13090
13151
  name,
13091
13152
  value: hasExpression || hasSvgUseHref || needsScoping
13092
13153
  ? codeGen.getStaticExpressionToken(attr)
13093
13154
  : value.value,
13155
+ elementName: element.name,
13094
13156
  };
13095
13157
  })
13096
13158
  .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.
13159
+ // See note above about `<input value>`/`<input checked>`
13101
13160
  element.properties
13102
13161
  .map((prop) => {
13162
+ const { attributeName, value } = prop;
13163
+ // Sanity check to ensure that only `<input value>`/`<input checked>` are treated as props
13164
+ /* v8 ignore start */
13165
+ if (process.env.NODE_ENV === 'test') {
13166
+ if (element.name !== 'input' &&
13167
+ !(attributeName === 'checked' || attributeName === 'value')) {
13168
+ throw new Error('Expected to only see `<input value>`/`<input checked>` here; instead found `<${element.name} ${attributeName}>');
13169
+ }
13170
+ }
13171
+ /* v8 ignore stop */
13103
13172
  return {
13104
- name: prop.attributeName,
13105
- value: prop.value.value,
13173
+ name: attributeName,
13174
+ value: value.value,
13175
+ isProp: true,
13106
13176
  };
13107
13177
  })
13108
13178
  .forEach(collector);
@@ -14155,17 +14225,29 @@ function transform(codeGen) {
14155
14225
  const children = parent.children;
14156
14226
  const childrenIterator = children[Symbol.iterator]();
14157
14227
  let current;
14228
+ function isTextOrIgnoredComment(node) {
14229
+ return isText(node) || (isComment(node) && !codeGen.preserveComments);
14230
+ }
14158
14231
  while ((current = childrenIterator.next()) && !current.done) {
14159
14232
  let child = current.value;
14160
- if (isText(child)) {
14233
+ // Concatenate contiguous text nodes together (while skipping ignored comments)
14234
+ // E.g. `<div>{foo}{bar}</div>` can be concatenated into a single text node expression,
14235
+ // and so can `<div>{foo}<!-- baz -->{bar}</div>` if comments are ignored.
14236
+ if (isTextOrIgnoredComment(child)) {
14161
14237
  const continuousText = [];
14162
14238
  // Consume all the contiguous text nodes.
14163
14239
  do {
14164
- continuousText.push(child);
14240
+ if (isText(child)) {
14241
+ continuousText.push(child);
14242
+ }
14165
14243
  current = childrenIterator.next();
14166
14244
  child = current.value;
14167
- } while (!current.done && isText(child));
14168
- res.push(transformText(continuousText));
14245
+ } while (!current.done && isTextOrIgnoredComment(child));
14246
+ // Only push an api_text call if we actually have text to render.
14247
+ // (We might just have iterated through a sequence of ignored comments.)
14248
+ if (continuousText.length) {
14249
+ res.push(transformText(continuousText));
14250
+ }
14169
14251
  // Early exit if a text node is the last child node.
14170
14252
  if (current.done) {
14171
14253
  break;
@@ -14636,5 +14718,5 @@ exports.generateScopeTokens = generateScopeTokens;
14636
14718
  exports.kebabcaseToCamelcase = kebabcaseToCamelcase;
14637
14719
  exports.parse = parse;
14638
14720
  exports.toPropertyName = toPropertyName;
14639
- /** version: 8.5.0 */
14721
+ /** version: 8.6.0 */
14640
14722
  //# sourceMappingURL=index.cjs.js.map