@eslint-react/jsx 5.8.11 → 5.8.12

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.d.ts CHANGED
@@ -2,6 +2,24 @@ import { TSESTreeJSXAttributeLike, TSESTreeJSXElementLike } from "@eslint-react/
2
2
  import { TSESTree } from "@typescript-eslint/types";
3
3
  import { RuleContext } from "@eslint-react/eslint";
4
4
 
5
+ //#region src/collapse-multiline-text.d.ts
6
+ /**
7
+ * Collapse a multiline JSX text string following React's whitespace rules.
8
+ *
9
+ * This mirrors Babel's `cleanJSXElementLiteralChild` algorithm:
10
+ * 1. Split the raw text into lines.
11
+ * 2. Find the last non-empty line.
12
+ * 3. Trim leading spaces on non-first lines and trailing spaces on non-last lines.
13
+ * 4. Collapse tabs into spaces.
14
+ * 5. Append a single space after each non-last non-empty line.
15
+ *
16
+ * @param text - The raw JSX text string to collapse.
17
+ * @returns The collapsed string, or `null` if the text contains only whitespace.
18
+ *
19
+ * @see https://github.com/babel/babel/blob/main/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
20
+ */
21
+ declare function collapseMultilineText(text: string): string | null;
22
+ //#endregion
5
23
  //#region src/find-attribute.d.ts
6
24
  /**
7
25
  * Find a JSX attribute (or spread attribute containing the property) by name
@@ -215,24 +233,12 @@ declare function getAttributeValue(context: RuleContext, element: TSESTree.JSXEl
215
233
  /**
216
234
  * Get the **meaningful** children of a JSX element or fragment.
217
235
  *
218
- * Filters out nodes that React will not render into the DOM:
219
- *
220
- * 1. "Padding spaces" — `JSXText` nodes that consist entirely of whitespace
221
- * and contain at least one newline (see {@link isWhitespace}). These are
222
- * code-formatting artefacts (indentation between tags). While React's client
223
- * renderer preserves them as text nodes, browser HTML parsers may discard
224
- * them during hydration, causing hydration mismatches.
225
- *
226
- * 2. Empty string expressions — `JSXExpressionContainer` nodes whose expression
227
- * is a string literal with value `""` (see {@link isEmptyStringExpression}).
228
- * React's reconciler and SSR renderer explicitly skip empty strings,
229
- * producing no DOM node.
230
- *
231
- * Whitespace-only text **without** a newline (e.g. the single space in
232
- * `<div> </div>`) is intentionally **kept**, because React renders it. For
233
- * this reason `getChildren(node).length > 0` is **not** equivalent to
234
- * {@link hasChildren}, which applies a stricter "any whitespace-only text is
235
- * non-meaningful" heuristic. Pick the one that matches your rule's intent.
236
+ * Mirrors Babel's `buildChildren` helper:
237
+ * 1. Iterate over `element.children`.
238
+ * 2. Skip `JSXText` nodes that clean to nothing (padding whitespace).
239
+ * 3. Skip `JSXExpressionContainer` nodes whose expression is empty.
240
+ * 4. Skip `JSXEmptyExpression` nodes.
241
+ * 5. Collect everything else.
236
242
  *
237
243
  * @param element - A `JSXElement` or `JSXFragment` node.
238
244
  * @returns An array of children nodes that contribute to rendered output.
@@ -505,8 +511,10 @@ declare function isHostElement(node: TSESTree.Node): node is TSESTree.JSXElement
505
511
  * trim away during rendering.
506
512
  *
507
513
  * A child is considered whitespace padding when it is a `JSXText` node whose
508
- * raw content is empty after trimming **and** contains at least one newline.
509
- * This is the whitespace that appears between JSX tags purely for formatting:
514
+ * content is empty after applying React's whitespace normalization
515
+ * (see {@link collapseMultilineText}, modelled after Babel's
516
+ * `cleanJSXElementLiteralChild`). This is the whitespace that appears between
517
+ * JSX tags purely for formatting:
510
518
  *
511
519
  * ```jsx
512
520
  * <div>
@@ -515,20 +523,8 @@ declare function isHostElement(node: TSESTree.Node): node is TSESTree.JSXElement
515
523
  * </div>
516
524
  * ```
517
525
  *
518
- * Use {@link isWhitespaceText} for a looser check that also matches
519
- * whitespace‑only text that does **not** contain a newline.
520
- *
521
526
  * @param node - A JSX child node.
522
527
  * @returns `true` when the node is purely formatting whitespace.
523
- *
524
- * @example
525
- * ```ts
526
- * import { isWhitespace } from "@eslint-react/jsx";
527
- *
528
- * const meaningful = element.children.filter(
529
- * (child) => !isWhitespace(child),
530
- * );
531
- * ```
532
528
  */
533
529
  declare function isWhitespace(node: TSESTree.JSXChild): boolean;
534
530
  /**
@@ -632,4 +628,4 @@ declare const DEFAULT_JSX_DETECTION_HINT: JsxDetectionHint;
632
628
  */
633
629
  declare function resolveAttributeValue(context: RuleContext, attribute: TSESTreeJSXAttributeLike): JsxAttributeValue;
634
630
  //#endregion
635
- export { DEFAULT_JSX_DETECTION_HINT, ElementTest, JsxAttributeValue, JsxDetectionHint, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespace, isWhitespaceText, resolveAttributeValue };
631
+ export { DEFAULT_JSX_DETECTION_HINT, ElementTest, JsxAttributeValue, JsxDetectionHint, collapseMultilineText, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespace, isWhitespaceText, resolveAttributeValue };
package/dist/index.js CHANGED
@@ -4,6 +4,44 @@ import { AST_NODE_TYPES } from "@typescript-eslint/types";
4
4
  import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
5
5
  import { P, match } from "ts-pattern";
6
6
 
7
+ //#region src/collapse-multiline-text.ts
8
+ /**
9
+ * Collapse a multiline JSX text string following React's whitespace rules.
10
+ *
11
+ * This mirrors Babel's `cleanJSXElementLiteralChild` algorithm:
12
+ * 1. Split the raw text into lines.
13
+ * 2. Find the last non-empty line.
14
+ * 3. Trim leading spaces on non-first lines and trailing spaces on non-last lines.
15
+ * 4. Collapse tabs into spaces.
16
+ * 5. Append a single space after each non-last non-empty line.
17
+ *
18
+ * @param text - The raw JSX text string to collapse.
19
+ * @returns The collapsed string, or `null` if the text contains only whitespace.
20
+ *
21
+ * @see https://github.com/babel/babel/blob/main/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
22
+ */
23
+ function collapseMultilineText(text) {
24
+ const lines = text.split(/\r\n|\n|\r/);
25
+ let lastNonEmptyLine = 0;
26
+ for (let i = 0; i < lines.length; i++) if (/[^ \t]/.exec(lines[i]) != null) lastNonEmptyLine = i;
27
+ let str = "";
28
+ for (let i = 0; i < lines.length; i++) {
29
+ const line = lines[i];
30
+ const isFirstLine = i === 0;
31
+ const isLastLine = i === lines.length - 1;
32
+ const isLastNonEmptyLine = i === lastNonEmptyLine;
33
+ let trimmedLine = line.replace(/\t/g, " ");
34
+ if (!isFirstLine) trimmedLine = trimmedLine.replace(/^ +/, "");
35
+ if (!isLastLine) trimmedLine = trimmedLine.replace(/ +$/, "");
36
+ if (trimmedLine.length > 0) {
37
+ if (!isLastNonEmptyLine) trimmedLine += " ";
38
+ str += trimmedLine;
39
+ }
40
+ }
41
+ return str || null;
42
+ }
43
+
44
+ //#endregion
7
45
  //#region src/get-attribute-name.ts
8
46
  /**
9
47
  * Get the stringified name of a `JSXAttribute` node.
@@ -299,8 +337,10 @@ function getAttributeValue(context, element, name) {
299
337
  * trim away during rendering.
300
338
  *
301
339
  * A child is considered whitespace padding when it is a `JSXText` node whose
302
- * raw content is empty after trimming **and** contains at least one newline.
303
- * This is the whitespace that appears between JSX tags purely for formatting:
340
+ * content is empty after applying React's whitespace normalization
341
+ * (see {@link collapseMultilineText}, modelled after Babel's
342
+ * `cleanJSXElementLiteralChild`). This is the whitespace that appears between
343
+ * JSX tags purely for formatting:
304
344
  *
305
345
  * ```jsx
306
346
  * <div>
@@ -309,24 +349,12 @@ function getAttributeValue(context, element, name) {
309
349
  * </div>
310
350
  * ```
311
351
  *
312
- * Use {@link isWhitespaceText} for a looser check that also matches
313
- * whitespace‑only text that does **not** contain a newline.
314
- *
315
352
  * @param node - A JSX child node.
316
353
  * @returns `true` when the node is purely formatting whitespace.
317
- *
318
- * @example
319
- * ```ts
320
- * import { isWhitespace } from "@eslint-react/jsx";
321
- *
322
- * const meaningful = element.children.filter(
323
- * (child) => !isWhitespace(child),
324
- * );
325
- * ```
326
354
  */
327
355
  function isWhitespace(node) {
328
356
  if (node.type !== AST_NODE_TYPES.JSXText) return false;
329
- return node.raw.trim() === "" && node.raw.includes("\n");
357
+ return collapseMultilineText(node.value) == null && node.value.includes("\n");
330
358
  }
331
359
  /**
332
360
  * Check whether a JSX child node is **any** whitespace‑only text.
@@ -375,24 +403,12 @@ function isEmptyStringExpression(node) {
375
403
  /**
376
404
  * Get the **meaningful** children of a JSX element or fragment.
377
405
  *
378
- * Filters out nodes that React will not render into the DOM:
379
- *
380
- * 1. "Padding spaces" — `JSXText` nodes that consist entirely of whitespace
381
- * and contain at least one newline (see {@link isWhitespace}). These are
382
- * code-formatting artefacts (indentation between tags). While React's client
383
- * renderer preserves them as text nodes, browser HTML parsers may discard
384
- * them during hydration, causing hydration mismatches.
385
- *
386
- * 2. Empty string expressions — `JSXExpressionContainer` nodes whose expression
387
- * is a string literal with value `""` (see {@link isEmptyStringExpression}).
388
- * React's reconciler and SSR renderer explicitly skip empty strings,
389
- * producing no DOM node.
390
- *
391
- * Whitespace-only text **without** a newline (e.g. the single space in
392
- * `<div> </div>`) is intentionally **kept**, because React renders it. For
393
- * this reason `getChildren(node).length > 0` is **not** equivalent to
394
- * {@link hasChildren}, which applies a stricter "any whitespace-only text is
395
- * non-meaningful" heuristic. Pick the one that matches your rule's intent.
406
+ * Mirrors Babel's `buildChildren` helper:
407
+ * 1. Iterate over `element.children`.
408
+ * 2. Skip `JSXText` nodes that clean to nothing (padding whitespace).
409
+ * 3. Skip `JSXExpressionContainer` nodes whose expression is empty.
410
+ * 4. Skip `JSXEmptyExpression` nodes.
411
+ * 5. Collect everything else.
396
412
  *
397
413
  * @param element - A `JSXElement` or `JSXFragment` node.
398
414
  * @returns An array of children nodes that contribute to rendered output.
@@ -412,11 +428,23 @@ function isEmptyStringExpression(node) {
412
428
  * ```
413
429
  */
414
430
  function getChildren(element) {
415
- return element.children.filter((child) => {
416
- if (isWhitespace(child)) return false;
417
- if (isEmptyStringExpression(child)) return false;
418
- return true;
419
- });
431
+ const elements = [];
432
+ for (const child of element.children) {
433
+ if (child.type === AST_NODE_TYPES.JSXText) {
434
+ if (collapseMultilineText(child.value) == null && child.value.includes("\n")) continue;
435
+ elements.push(child);
436
+ continue;
437
+ }
438
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
439
+ const { expression } = child;
440
+ if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) continue;
441
+ if (isEmptyStringExpression(child)) continue;
442
+ elements.push(child);
443
+ continue;
444
+ }
445
+ elements.push(child);
446
+ }
447
+ return elements;
420
448
  }
421
449
 
422
450
  //#endregion
@@ -723,4 +751,4 @@ const JsxDetectionHint = {
723
751
  const DEFAULT_JSX_DETECTION_HINT = 0n | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue;
724
752
 
725
753
  //#endregion
726
- export { DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespace, isWhitespaceText, resolveAttributeValue };
754
+ export { DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, collapseMultilineText, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespace, isWhitespaceText, resolveAttributeValue };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-react/jsx",
3
- "version": "5.8.11",
3
+ "version": "5.8.12",
4
4
  "description": "ESLint React's TSESTree JSX utility module for static analysis of JSX patterns.",
5
5
  "homepage": "https://github.com/Rel1cx/eslint-react",
6
6
  "bugs": {
@@ -32,10 +32,10 @@
32
32
  "@typescript-eslint/types": "^8.60.1",
33
33
  "@typescript-eslint/utils": "^8.60.1",
34
34
  "ts-pattern": "^5.9.0",
35
- "@eslint-react/ast": "5.8.11",
36
- "@eslint-react/eslint": "5.8.11",
37
- "@eslint-react/shared": "5.8.11",
38
- "@eslint-react/var": "5.8.11"
35
+ "@eslint-react/ast": "5.8.12",
36
+ "@eslint-react/shared": "5.8.12",
37
+ "@eslint-react/var": "5.8.12",
38
+ "@eslint-react/eslint": "5.8.12"
39
39
  },
40
40
  "devDependencies": {
41
41
  "eslint": "^10.4.1",