benjamn / benjamn/recast

JSXText node leading space is not preserved

Open
#886 3 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5.3k
Forks
364
Avg merge
3d 8h
Merged PRs (30d)
3

Description

I'm working with jscodeshift, and noticed an issue when re-printing an existing node. I'm creating a codemod to convert a JSXElement into another type of JSXElement, however, I'd like to re-use the entire children array. Doing so results in a functional change to the code (leading space from JSXText node is removed).

Here's a basic script to convey what I'm attempting with the latest version (recast@0.20.4):

const { parse, print, types } = require("recast");

const b = types.builders; // alias

// Parsed.
const source = `
<original>
  <span role="img">📝</span> Text
</original>`;
const parsedJsxElement = parse(source).program.body[0].expression;

// Builders.
const builtJsxElement = b.jsxElement(
  b.jsxOpeningElement(b.jsxIdentifier("New")),
  b.jsxClosingElement(b.jsxIdentifier("New")),
  [
    b.jsxText("\n  "),
    b.jsxElement(
      b.jsxOpeningElement(b.jsxIdentifier("span")),
      b.jsxClosingElement(b.jsxIdentifier("span")),
      [b.jsxText("📝")]
    ),
    b.jsxText(" Text\n"),
  ]
);

// Built, but reusing children from the parsed source.
const builtJsxElementWithReusedChildren = b.jsxElement(
  b.jsxOpeningElement(b.jsxIdentifier("New")),
  b.jsxClosingElement(b.jsxIdentifier("New")),
  parsedJsxElement.children
);

console.log("=== Parsed JSX Element ===");
console.log(print(parsedJsxElement).code);
// <original>
//   <span role="img">📝</span> Text
// </original>

console.log("=== Built JSX Element ===");
console.log(print(builtJsxElement).code);
// <New>
//     <span>📝</span>Text</New>

console.log("=== Built JSX Element w/ Reused Children ===");
console.log(print(builtJsxElementWithReusedChildren).code);
// <New>
//     <span role="img">📝</span>Text</New>

// Mutation.
parsedJsxElement.openingElement = b.jsxOpeningElement(b.jsxIdentifier("New"));
parsedJsxElement.closingElement = b.jsxClosingElement(b.jsxIdentifier("New"));

console.log("=== Mutated Parsed JSX Element ===");
console.log(print(parsedJsxElement).code);
// <New>
//   <span role="img">📝</span> Text
// </New>

Output

=== Parsed JSX Element ===
<original>
  <span role="img">📝</span> Text
</original>
=== Built JSX Element ===
<New>
    <span>📝</span>Text</New>
=== Built JSX Element w/ Reused Children ===
<New>
    <span role="img">📝</span>Text</New>
=== Mutated Parsed JSX Element ===
<New>
  <span role="img">📝</span> Text
</New>

The only approach I've found to work is directly mutating the original node. Is it possible to construct an entirely new JSXElement, or reuse children from another node while retaining the spacing between the closing span and Text (</span>[SPACE]Text)? Reusing children directly would be ideal in this case since the exact child nodes can vary.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the supplied parse/print/types reproduction against recast@0.20.4, focusing on JSXElement and JSXText children reused through the builders. Done means constructing a new JSXElement preserves the space between the closing span and Text, matching the parsed and mutated output.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.