Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve jsx #559

Closed
aleclarson opened this issue Nov 15, 2020 · 5 comments · Fixed by #788
Closed

Preserve jsx #559

aleclarson opened this issue Nov 15, 2020 · 5 comments · Fixed by #788

Comments

@aleclarson
Copy link
Contributor

Is there a way for sucrase to parse jsx but not transform it? Thanks

@alangpierce
Copy link
Owner

Sorry for the delay in getting to this. Unfortunately that's not configurable right now, though you can hack it yourself by calling into Sucrase internals. Here's an example that copies and tweaks the src/index.ts code to parse JSX and TypeScript but only transforms TypeScript:

import {HelperManager} from "sucrase/dist/HelperManager";
import identifyShadowedGlobals from "sucrase/dist/identifyShadowedGlobals";
import NameManager from "sucrase/dist/NameManager";
import {parse} from "sucrase/dist/parser";
import TokenProcessor from "sucrase/dist/TokenProcessor";
import RootTransformer from "sucrase/dist/transformers/RootTransformer";
import getTSImportedNames from "sucrase/dist/util/getTSImportedNames";

function transformTSOnly(code: string): string {
  const {tokens, scopes} = parse(
    code,
    true /* isJSXEnabled */,
    true /* isTypeScriptEnabled */,
    false /* isFlowEnabled */,
  );
  const nameManager = new NameManager(code, tokens);
  const helperManager = new HelperManager(nameManager);
  const tokenProcessor = new TokenProcessor(code, tokens, false /* isFlowEnabled */, helperManager);

  identifyShadowedGlobals(tokenProcessor, scopes, getTSImportedNames(tokenProcessor));
  const sucraseContext = {
    tokenProcessor,
    scopes,
    nameManager,
    importProcessor: null,
    helperManager,
  };

  const transformer = new RootTransformer(sucraseContext, ["typescript"], false, {
    transforms: ["typescript"],
  });
  return transformer.transform();
}

console.log(
  transformTSOnly(`
import React from 'react';

interface LabelProps {
  name: string;
}

function Label({name}: LabelProps): JSX.Element {
  return (
    <div className="foo">
      Hello {name}
    </div>
  );
}
`),
);

Of course, any upgrade to Sucrase should be considered a potentially-breaking change for code like this.

What's your use case? And did I understand your goal correctly? Generally I think of Sucrase as targeting browsers and node, where AFAIU there's never native support for JSX.

I could see two ways to add built-in support here:

  • Add a special option for this use case, like parseJSX (with the transform unspecified). I'm a bit hesitant about this direction because I want to avoid configuration complexity as much as possible, but if it's more of an optional thing it could be reasonable.
  • Make the Sucrase parser lenient so that it always parses JSX. Unfortunately this is hard for typescript parsing because ts and tsx are non-overlapping because tsx disallows <number>foo type assertions. But it may be possible to get the parser to be able to handle both. That would involve combining the cases of tsParseMaybeAssign using restoreFromSnapshot to backtrack if JSX parsing fails. I'm also thinking of dropping Flow support at some point, so that could get the parser to a zero-configuration state.

@ScottAwesome
Copy link

if I were to speculate here, I think the use case would be preserving the JSX/TSX so it can be handed off to another transpiler, there are many babel plugins alone that take advantage of the JSX syntax tree and do transformations, but pre-transform the JSX into JS, you lose access to all that. I reckon this is the number1 motivator for a change like this.

@milahu
Copy link

milahu commented Nov 16, 2021

I think the use case would be preserving the JSX/TSX so it can be handed off to another transpiler

yepp, in my case: solidjs. react has no monopoly on jsx ; )

example: convert tsx to jsx files in solid-blocks

Here's an example

fixed version
/*
tsx2jsx.mjs
convert typescript to vanillajs (actual javascript)

npm i -D sucrase tiny-glob
node tsx2jsx.mjs

https://github.com/alangpierce/sucrase/issues/559
https://github.com/milahu/random/blob/master/javascript/tsx2jsx.mjs
*/

import fs from 'fs';
import {spawnSync} from 'child_process';

import glob from 'tiny-glob';

import {HelperManager} from "sucrase/dist/HelperManager.js";
import identifyShadowedGlobalsModule from "sucrase/dist/identifyShadowedGlobals.js";
import NameManagerModule from "sucrase/dist/NameManager.js";
import {parse} from "sucrase/dist/parser/index.js";
import TokenProcessorModule from "sucrase/dist/TokenProcessor.js";
import RootTransformerModule from "sucrase/dist/transformers/RootTransformer.js";
import getTSImportedNamesModule from "sucrase/dist/util/getTSImportedNames.js";

// workaround ...
// TypeError: NameManager is not a constructor
const NameManager = NameManagerModule.default;
const TokenProcessor = TokenProcessorModule.default;
const getTSImportedNames = getTSImportedNamesModule.default;
const identifyShadowedGlobals = identifyShadowedGlobalsModule.default;
const RootTransformer = RootTransformerModule.default;

function transformTSOnly(code) {
  const {tokens, scopes} = parse(
    code,
    true /* isJSXEnabled */,
    true /* isTypeScriptEnabled */,
    false /* isFlowEnabled */,
  );
  const nameManager = new NameManager(code, tokens);
  const helperManager = new HelperManager(nameManager);
  const tokenProcessor = new TokenProcessor(code, tokens, false /* isFlowEnabled */, helperManager);

  identifyShadowedGlobals(tokenProcessor, scopes, getTSImportedNames(tokenProcessor));
  const sucraseContext = {
    tokenProcessor,
    scopes,
    nameManager,
    importProcessor: null,
    helperManager,
  };

  // https://github.com/alangpierce/sucrase#transforms
  const sucraseOptions = {
    transforms: ["typescript"],
    disableESTransforms: true, // keep modern javascript: Optional chaining, Nullish coalescing, ...
  };

  const transformer = new RootTransformer(sucraseContext, ["typescript"], false, sucraseOptions);
  return transformer.transform();
}

const gitDirty = spawnSync('git', ['diff-index', 'HEAD', '--'], { encoding: 'utf8' }).stdout;
if (gitDirty != '') {
  console.log(`error: git is dirty:\n\n${gitDirty}`);
  process.exit(1);
}

const todoTransform = [];
//for (const fi of await glob('./src/**/*.tsx')) { // convert all files in src/ folder
for (const fi of await glob('./**/*.tsx')) { // convert all files in workdir
  const fo = fi.slice(0, -4) + '.jsx';
  console.log(`rename: ${fi} -> ${fo}`);
  spawnSync('git', ['mv', '-v', fi, fo]); // rename
  todoTransform.push([fi, fo]);
}
spawnSync('git', ['commit', '-m', 'tsx2jsx: rename']); // commit

for (const [fi, fo] of todoTransform) {
  console.log(`transform: ${fi} -> ${fo}`);
  const i = fs.readFileSync(fo, 'utf8');
  const o = transformTSOnly(i);
  // always do your backups :P
  fs.writeFileSync(fo, o, 'utf8'); // replace
  spawnSync('git', ['add', fo]); // add
}
spawnSync('git', ['commit', '-m', 'tsx2jsx: transform']); // commit

console.log(`
next steps:

git diff HEAD^  # inspect transform
git reset --hard HEAD~2  # undo transform + rename
`);

edit: i was looking for disableESTransforms

alternative: npx tsc -p tsconfig.json

tsconfig.json
{
  "include": [ "./src" ],
  "compilerOptions": {
    "outDir": "./tsc-out",
    "target": "es2020",
    "module": "es2020",
    "strict": false,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "moduleResolution": "node"
  }
}

@xixixao
Copy link

xixixao commented Nov 28, 2021

To strip TypeScript:

  1. Run npm install sucrase
  2. in node_modules/sucrase/dist/index.js replace isJSXEnabled with true in the _parser.parse.call( call
  3. in node_modules/sucrase/dist/cli.js disableESTransforms: true, to sucraseOptions: { object
  4. Run npx sucrase <SOURCE_DIR> -d <OUT_DIR> --transforms typescript
  5. To reformat run npx prettier --write <OUT_DIR>

You get ES6 without TypeScript, without needing the TypeScript compiler or installing all the project dependencies 🥳

alangpierce added a commit that referenced this issue Mar 25, 2023
Fixes #780
Fixes #559

This PR adds support for preserving JSX as-is. Since the parser need to know if
JSX is enabled or not, preserving JSX is accomplished by keeping `"jsx"` as a
transform, but setting the `jsxRuntime` mode to `"preserve"`.

Most of the details just work: the imports and typescript transform are mostly
able to remove type syntax and transform imported names properly. The one
missing case was handling situations like `<div>`, which is not actually an
identifier access and thus should not be transformed by the imports transform.
This required adding a special case to the parser to remove the identifier role
in that case.

For now, the ts-node integration does not recognize this option because Node
wouldn't be able to recognize JSX anyway.
@alangpierce
Copy link
Owner

I just released support for this in 3.31.0. To preserve JSX, include jsx in the list of transforms and set jsxRuntime: "preserve" in the configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants