@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.js CHANGED
@@ -11366,6 +11366,8 @@ function isAttribute(element, attrName) {
11366
11366
  }
11367
11367
  // Handle input tag value="" and checked attributes that are only used for state initialization.
11368
11368
  // Because .setAttribute() won't update the value, those attributes should be considered as props.
11369
+ // Note: this is tightly-coupled with static-element-serializer.ts which treats `<input checked="...">`
11370
+ // and `<input value="...">` as special because of the logic below.
11369
11371
  if (element.name === 'input' && (attrName === 'value' || attrName === 'checked')) {
11370
11372
  return false;
11371
11373
  }
@@ -11539,6 +11541,7 @@ function parseElement(ctx, parse5Elm, parentNode, parse5ParentLocation) {
11539
11541
  applyKey(ctx, parsedAttr, element);
11540
11542
  applyLwcDirectives(ctx, parsedAttr, element);
11541
11543
  applyAttributes(ctx, parsedAttr, element);
11544
+ validateSlotAttribute(ctx, parsedAttr, parentNode, element);
11542
11545
  validateElement(ctx, element, parse5Elm);
11543
11546
  validateAttributes(ctx, parsedAttr, element);
11544
11547
  validateProperties(ctx, element);
@@ -12541,6 +12544,31 @@ function validateAttributes(ctx, parsedAttr, element) {
12541
12544
  }
12542
12545
  }
12543
12546
  }
12547
+ function validateSlotAttribute(ctx, parsedAttr, parentNode, element) {
12548
+ const slotAttr = parsedAttr.get('slot');
12549
+ if (!slotAttr) {
12550
+ return;
12551
+ }
12552
+ function isElementOrSlot(node) {
12553
+ return isElement(node) || isSlot(node);
12554
+ }
12555
+ // Find the nearest ancestor that is an element or `<slot>`, and stop if we hit a component.
12556
+ // E.g. this should warn due to the `<div>`: `<x-foo><div><span slot=bar></span></div></x-foo>`
12557
+ // And this should _not_ warn: `<div><x-foo><span slot=bar></span></x-foo></div>`
12558
+ const elementOrSlotAncestor = ctx.findAncestor(isElementOrSlot, ({ current }) => current && !isComponent(current) && !isExternalComponent(current), parentNode);
12559
+ // Warn if a `slot` attribute is on an element that isn't an immediate child of a containing LWC component or
12560
+ // `lwc:external` component. This is a case that all three of native-shadow/synthetic-shadow/light DOM will
12561
+ // simply ignore, but it's good to warn, so that developers realize that they may be making a mistake.
12562
+ // Note that, for the purposes of being considered an "immediate child," virtual elements like `for:each` and
12563
+ // `lwc:if` don't count - only rendered elements (including `<slot>`s) count.
12564
+ // Example of invalid usage: `<x-foo><div><span slot=bar></span></div></x-foo>`
12565
+ if (elementOrSlotAncestor) {
12566
+ ctx.warnOnNode(ParserDiagnostics.IGNORED_SLOT_ATTRIBUTE_IN_CHILD, slotAttr, [
12567
+ `<${element.name}>`,
12568
+ `<${elementOrSlotAncestor.name}>`,
12569
+ ]);
12570
+ }
12571
+ }
12544
12572
  function validateProperties(ctx, element) {
12545
12573
  for (const prop of element.properties) {
12546
12574
  const { attributeName: attrName, value } = prop;
@@ -12987,14 +13015,33 @@ function serializeAttrs(element, codeGen) {
12987
13015
  */
12988
13016
  const attrs = [];
12989
13017
  let hasClassAttr = false;
12990
- const collector = ({ name, value, hasExpression, hasSvgUseHref, needsScoping, }) => {
13018
+ const collector = ({ name, value, isProp, hasExpression, hasSvgUseHref, needsScoping, }) => {
12991
13019
  // Do not serialize boolean class/style attribute (consistent with non-static optimized)
12992
13020
  if (typeof value === 'boolean' && (name === 'class' || name === 'style')) {
12993
13021
  return;
12994
13022
  }
12995
13023
  // See W-16614169
12996
13024
  const escapedAttributeName = templateStringEscape(name);
12997
- if (typeof value === 'string') {
13025
+ // `<input checked="...">` and `<input value="...">` have a peculiar attr/prop relationship, so the engine
13026
+ // has historically treated them as props rather than attributes:
13027
+ // https://github.com/salesforce/lwc/blob/b584d39/packages/%40lwc/template-compiler/src/parser/attribute.ts#L217-L221
13028
+ // For example, an element might be rendered as `<input type=checkbox>` but `input.checked` could
13029
+ // still return true. `value` behaves similarly. `value` and `checked` behave surprisingly
13030
+ // because the attributes actually represent the "default" value rather than the current one:
13031
+ // - https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields
13032
+ // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#checked
13033
+ if (isProp) {
13034
+ // The below logic matches the behavior of non-static-optimized DOM nodes.
13035
+ // There is no functional difference between e.g. `checked="checked"` and `checked` but we
13036
+ // match byte-for-byte the non-static-optimized HTML that would be rendered.
13037
+ if (name === 'checked' || (name === 'value' && value === '')) {
13038
+ attrs.push(` ${escapedAttributeName}`);
13039
+ }
13040
+ else {
13041
+ attrs.push(` ${escapedAttributeName}="${htmlEscape(String(value), true)}"`);
13042
+ }
13043
+ }
13044
+ else if (typeof value === 'string') {
12998
13045
  let v = templateStringEscape(value);
12999
13046
  if (name === 'class') {
13000
13047
  // ${0} maps to class token that will be appended to the string.
@@ -13030,12 +13077,25 @@ function serializeAttrs(element, codeGen) {
13030
13077
  // Skip serializing here and handle it as if it were a dynamic attribute instead.
13031
13078
  // Note that, to maintain backwards compatibility with the non-static output, we treat the valueless
13032
13079
  // "boolean" format (e.g. `<div id>`) as the empty string, which is semantically equivalent.
13080
+ // `isProp` corresponds to `value` or `checked` on an `<input>` which is treated as a prop at runtime.
13033
13081
  const needsPlaceholder = hasExpression || hasSvgUseHref || needsScoping;
13034
- // Inject a placeholder where the staticPartId will go when an expression occurs.
13035
- // This is only needed for SSR to inject the expression value during serialization.
13036
- attrs.push(needsPlaceholder
13037
- ? `\${"${v}"}`
13038
- : ` ${escapedAttributeName}="${htmlEscape(v, true)}"`);
13082
+ let nameAndValue;
13083
+ if (needsPlaceholder) {
13084
+ // Inject a placeholder where the staticPartId will go when an expression occurs.
13085
+ // This is only needed for SSR to inject the expression value during serialization.
13086
+ nameAndValue = `\${"${v}"}`;
13087
+ }
13088
+ else if (v === '') {
13089
+ // In HTML, there is no difference between the empty string value (`<div foo="">`) and "boolean true"
13090
+ // (`<div foo>`). They are both parsed identically, and the DOM treats them the same (`getAttribute`
13091
+ // returns the empty string). Here we prefer the shorter format.
13092
+ // https://html.spec.whatwg.org/multipage/introduction.html#a-quick-introduction-to-html:syntax-attributes
13093
+ nameAndValue = ` ${escapedAttributeName}`;
13094
+ }
13095
+ else {
13096
+ nameAndValue = ` ${escapedAttributeName}="${htmlEscape(v, true)}"`;
13097
+ }
13098
+ attrs.push(nameAndValue);
13039
13099
  }
13040
13100
  else {
13041
13101
  attrs.push(` ${escapedAttributeName}`);
@@ -13062,23 +13122,33 @@ function serializeAttrs(element, codeGen) {
13062
13122
  return {
13063
13123
  hasExpression,
13064
13124
  hasSvgUseHref,
13125
+ isProp: false,
13065
13126
  needsScoping,
13066
13127
  name,
13067
13128
  value: hasExpression || hasSvgUseHref || needsScoping
13068
13129
  ? codeGen.getStaticExpressionToken(attr)
13069
13130
  : value.value,
13131
+ elementName: element.name,
13070
13132
  };
13071
13133
  })
13072
13134
  .forEach(collector);
13073
- // This is tightly coupled with the logic in the parser that decides when an attribute should be
13074
- // a property: https://github.com/salesforce/lwc/blob/master/packages/%40lwc/template-compiler/src/parser/attribute.ts#L198-L218
13075
- // Because a component can't be a static element, we only look in the property bag on value and checked attribute
13076
- // from the input.
13135
+ // See note above about `<input value>`/`<input checked>`
13077
13136
  element.properties
13078
13137
  .map((prop) => {
13138
+ const { attributeName, value } = prop;
13139
+ // Sanity check to ensure that only `<input value>`/`<input checked>` are treated as props
13140
+ /* v8 ignore start */
13141
+ if (process.env.NODE_ENV === 'test') {
13142
+ if (element.name !== 'input' &&
13143
+ !(attributeName === 'checked' || attributeName === 'value')) {
13144
+ throw new Error('Expected to only see `<input value>`/`<input checked>` here; instead found `<${element.name} ${attributeName}>');
13145
+ }
13146
+ }
13147
+ /* v8 ignore stop */
13079
13148
  return {
13080
- name: prop.attributeName,
13081
- value: prop.value.value,
13149
+ name: attributeName,
13150
+ value: value.value,
13151
+ isProp: true,
13082
13152
  };
13083
13153
  })
13084
13154
  .forEach(collector);
@@ -14131,17 +14201,29 @@ function transform(codeGen) {
14131
14201
  const children = parent.children;
14132
14202
  const childrenIterator = children[Symbol.iterator]();
14133
14203
  let current;
14204
+ function isTextOrIgnoredComment(node) {
14205
+ return isText(node) || (isComment(node) && !codeGen.preserveComments);
14206
+ }
14134
14207
  while ((current = childrenIterator.next()) && !current.done) {
14135
14208
  let child = current.value;
14136
- if (isText(child)) {
14209
+ // Concatenate contiguous text nodes together (while skipping ignored comments)
14210
+ // E.g. `<div>{foo}{bar}</div>` can be concatenated into a single text node expression,
14211
+ // and so can `<div>{foo}<!-- baz -->{bar}</div>` if comments are ignored.
14212
+ if (isTextOrIgnoredComment(child)) {
14137
14213
  const continuousText = [];
14138
14214
  // Consume all the contiguous text nodes.
14139
14215
  do {
14140
- continuousText.push(child);
14216
+ if (isText(child)) {
14217
+ continuousText.push(child);
14218
+ }
14141
14219
  current = childrenIterator.next();
14142
14220
  child = current.value;
14143
- } while (!current.done && isText(child));
14144
- res.push(transformText(continuousText));
14221
+ } while (!current.done && isTextOrIgnoredComment(child));
14222
+ // Only push an api_text call if we actually have text to render.
14223
+ // (We might just have iterated through a sequence of ignored comments.)
14224
+ if (continuousText.length) {
14225
+ res.push(transformText(continuousText));
14226
+ }
14145
14227
  // Early exit if a text node is the last child node.
14146
14228
  if (current.done) {
14147
14229
  break;
@@ -14607,5 +14689,5 @@ function compile(source, filename, config) {
14607
14689
  }
14608
14690
 
14609
14691
  export { ElementDirectiveName, LWCDirectiveDomMode, LWCDirectiveRenderMode, LwcTagName, RootDirectiveName, TemplateDirectiveName, compile, compile as default, generateScopeTokens, kebabcaseToCamelcase, parse, toPropertyName };
14610
- /** version: 8.5.0 */
14692
+ /** version: 8.6.0 */
14611
14693
  //# sourceMappingURL=index.js.map