Giter Club home page Giter Club logo

sucrase's Introduction

Sucrase

Build Status npm version Install Size MIT License Join the chat at https://gitter.im/sucrasejs

Quick usage

yarn add --dev sucrase  # Or npm install --save-dev sucrase
node -r sucrase/register main.ts

Using the ts-node integration:

yarn add --dev sucrase ts-node typescript
./node_modules/.bin/ts-node --transpiler sucrase/ts-node-plugin main.ts

Project overview

Sucrase is an alternative to Babel that allows super-fast development builds. Instead of compiling a large range of JS features to be able to work in Internet Explorer, Sucrase assumes that you're developing with a recent browser or recent Node.js version, so it focuses on compiling non-standard language extensions: JSX, TypeScript, and Flow. Because of this smaller scope, Sucrase can get away with an architecture that is much more performant but less extensible and maintainable. Sucrase's parser is forked from Babel's parser (so Sucrase is indebted to Babel and wouldn't be possible without it) and trims it down to a focused subset of what Babel solves. If it fits your use case, hopefully Sucrase can speed up your development experience!

Sucrase has been extensively tested. It can successfully build the Benchling frontend code, Babel, React, TSLint, Apollo client, and decaffeinate with all tests passing, about 1 million lines of code total.

Sucrase is about 20x faster than Babel. Here's one measurement of how Sucrase compares with other tools when compiling the Jest codebase 3 times, about 360k lines of code total:

            Time            Speed
Sucrase     0.57 seconds    636975 lines per second
swc         1.19 seconds    304526 lines per second
esbuild     1.45 seconds    248692 lines per second
TypeScript  8.98 seconds    40240 lines per second
Babel       9.18 seconds    39366 lines per second

Details: Measured on July 2022. Tools run in single-threaded mode without warm-up. See the benchmark code for methodology and caveats.

Transforms

The main configuration option in Sucrase is an array of transform names. These transforms are available:

  • jsx: Enables JSX syntax. By default, JSX is transformed to React.createClass, but may be preserved or transformed to _jsx() by setting the jsxRuntime option. Also adds createReactClass display names and JSX context information.
  • typescript: Compiles TypeScript code to JavaScript, removing type annotations and handling features like enums. Does not check types. Sucrase transforms each file independently, so you should enable the isolatedModules TypeScript flag so that the typechecker will disallow the few features like const enums that need cross-file compilation. The Sucrase option keepUnusedImports can be used to disable all automatic removal of imports and exports, analogous to TS verbatimModuleSyntax.
  • flow: Removes Flow type annotations. Does not check types.
  • imports: Transforms ES Modules (import/export) to CommonJS (require/module.exports) using the same approach as Babel and TypeScript with --esModuleInterop. If preserveDynamicImport is specified in the Sucrase options, then dynamic import expressions are left alone, which is particularly useful in Node to load ESM-only libraries. If preserveDynamicImport is not specified, import expressions are transformed into a promise-wrapped call to require.
  • react-hot-loader: Performs the equivalent of the react-hot-loader/babel transform in the react-hot-loader project. This enables advanced hot reloading use cases such as editing of bound methods.
  • jest: Hoist desired jest method calls above imports in the same way as babel-plugin-jest-hoist. Does not validate the arguments passed to jest.mock, but the same rules still apply.

When the imports transform is not specified (i.e. when targeting ESM), the injectCreateRequireForImportRequire option can be specified to transform TS import foo = require("foo"); in a way that matches the TypeScript 4.7 behavior with module: nodenext.

These newer JS features are transformed by default:

If your target runtime supports these features, you can specify disableESTransforms: true so that Sucrase preserves the syntax rather than trying to transform it. Note that transpiled and standard class fields behave slightly differently; see the TypeScript 3.7 release notes for details. If you use TypeScript, you can enable the TypeScript option useDefineForClassFields to enable error checking related to these differences.

Unsupported syntax

All JS syntax not mentioned above will "pass through" and needs to be supported by your JS runtime. For example:

  • Decorators, private fields, throw expressions, generator arrow functions, and do expressions are all unsupported in browsers and Node (as of this writing), and Sucrase doesn't make an attempt to transpile them.
  • Object rest/spread, async functions, and async iterators are all recent features that should work fine, but might cause issues if you use older versions of tools like webpack. BigInt and newer regex features may or may not work, based on your tooling.

JSX Options

By default, JSX is compiled to React functions in development mode. This can be configured with a few options:

  • jsxRuntime: A string specifying the transform mode, which can be one of three values:
    • "classic" (default): The original JSX transform that calls React.createElement by default. To configure for non-React use cases, specify:
      • jsxPragma: Element creation function, defaults to React.createElement.
      • jsxFragmentPragma: Fragment component, defaults to React.Fragment.
    • "automatic": The new JSX transform introduced with React 17, which calls jsx functions and auto-adds import statements. To configure for non-React use cases, specify:
      • jsxImportSource: Package name for auto-generated import statements, defaults to react.
    • "preserve": Don't transform JSX, and instead emit it as-is in the output code.
  • production: If true, use production version of functions and don't include debugging information. When using React in production mode with the automatic transform, this must be set to true to avoid an error about jsxDEV being missing.

Legacy CommonJS interop

Two legacy modes can be used with the imports transform:

  • enableLegacyTypeScriptModuleInterop: Use the default TypeScript approach to CommonJS interop instead of assuming that TypeScript's --esModuleInterop flag is enabled. For example, if a CJS module exports a function, legacy TypeScript interop requires you to write import * as add from './add';, while Babel, Webpack, Node.js, and TypeScript with --esModuleInterop require you to write import add from './add';. As mentioned in the docs, the TypeScript team recommends you always use --esModuleInterop.
  • enableLegacyBabel5ModuleInterop: Use the Babel 5 approach to CommonJS interop, so that you can run require('./MyModule') instead of require('./MyModule').default. Analogous to babel-plugin-add-module-exports.

Usage

Tool integrations

Usage in Node

The most robust way is to use the Sucrase plugin for ts-node, which has various Node integrations and configures Sucrase via tsconfig.json:

ts-node --transpiler sucrase/ts-node-plugin

For projects that don't target ESM, Sucrase also has a require hook with some reasonable defaults that can be accessed in a few ways:

  • From code: require("sucrase/register");
  • When invoking Node: node -r sucrase/register main.ts
  • As a separate binary: sucrase-node main.ts

Options can be passed to the require hook via a SUCRASE_OPTIONS environment variable holding a JSON string of options.

Compiling a project to JS

For simple use cases, Sucrase comes with a sucrase CLI that mirrors your directory structure to an output directory:

sucrase ./srcDir -d ./outDir --transforms typescript,imports

Usage from code

For any advanced use cases, Sucrase can be called from JS directly:

import {transform} from "sucrase";
const compiledCode = transform(code, {transforms: ["typescript", "imports"]}).code;

What Sucrase is not

Sucrase is intended to be useful for the most common cases, but it does not aim to have nearly the scope and versatility of Babel. Some specific examples:

  • Sucrase does not check your code for errors. Sucrase's contract is that if you give it valid code, it will produce valid JS code. If you give it invalid code, it might produce invalid code, it might produce valid code, or it might give an error. Always use Sucrase with a linter or typechecker, which is more suited for error-checking.
  • Sucrase is not pluginizable. With the current architecture, transforms need to be explicitly written to cooperate with each other, so each additional transform takes significant extra work.
  • Sucrase is not good for prototyping language extensions and upcoming language features. Its faster architecture makes new transforms more difficult to write and more fragile.
  • Sucrase will never produce code for old browsers like IE. Compiling code down to ES5 is much more complicated than any transformation that Sucrase needs to do.
  • Sucrase is hesitant to implement upcoming JS features, although some of them make sense to implement for pragmatic reasons. Its main focus is on language extensions (JSX, TypeScript, Flow) that will never be supported by JS runtimes.
  • Like Babel, Sucrase is not a typechecker, and must process each file in isolation. For example, TypeScript const enums are treated as regular enums rather than inlining across files.
  • You should think carefully before using Sucrase in production. Sucrase is mostly beneficial in development, and in many cases, Babel or tsc will be more suitable for production builds.

See the Project Vision document for more details on the philosophy behind Sucrase.

Motivation

As JavaScript implementations mature, it becomes more and more reasonable to disable Babel transforms, especially in development when you know that you're targeting a modern runtime. You might hope that you could simplify and speed up the build step by eventually disabling Babel entirely, but this isn't possible if you're using a non-standard language extension like JSX, TypeScript, or Flow. Unfortunately, disabling most transforms in Babel doesn't speed it up as much as you might expect. To understand, let's take a look at how Babel works:

  1. Tokenize the input source code into a token stream.
  2. Parse the token stream into an AST.
  3. Walk the AST to compute the scope information for each variable.
  4. Apply all transform plugins in a single traversal, resulting in a new AST.
  5. Print the resulting AST.

Only step 4 gets faster when disabling plugins, so there's always a fixed cost to running Babel regardless of how many transforms are enabled.

Sucrase bypasses most of these steps, and works like this:

  1. Tokenize the input source code into a token stream using a trimmed-down fork of the Babel parser. This fork does not produce a full AST, but still produces meaningful token metadata specifically designed for the later transforms.
  2. Scan through the tokens, computing preliminary information like all imported/exported names.
  3. Run the transform by doing a pass through the tokens and performing a number of careful find-and-replace operations, like replacing <Foo with React.createElement(Foo.

Because Sucrase works on a lower level and uses a custom parser for its use case, it is much faster than Babel.

Contributing

Contributions are welcome, whether they be bug reports, PRs, docs, tests, or anything else! Please take a look through the Contributing Guide to learn how to get started.

License and attribution

Sucrase is MIT-licensed. A large part of Sucrase is based on a fork of the Babel parser, which is also MIT-licensed.

Why the name?

Sucrase is an enzyme that processes sugar. Get it?

sucrase's People

Contributors

0xflotus avatar alangpierce avatar aleclarson avatar antfu avatar arv avatar benjdlambert avatar commanderroot avatar cpitt avatar dependabot[bot] avatar forivall avatar itsbth avatar janmeier avatar jozanza avatar kesla avatar lobsterkatie avatar matthewp avatar matthieugicquel avatar nihgwu avatar oliveira-andre avatar pnappa avatar raynos avatar ricardobeat avatar rich-harris avatar rugvip avatar skacekamen avatar srmagura avatar styfle avatar sudowork avatar tombyrer avatar webschik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sucrase's Issues

Why are __self and __source passed as props in compiled JSX code?

ReactDOM.render(<div className={css(styles.header)}>
    <h1>{foobar}</h1>
</div>, container);

compiles to

ReactDOM.render(React.createElement('div', { className: (0, _aphrodite.css)(styles.header), __self: this, __source: {fileName: _jsxFileName, lineNumber: 16}}
    , React.createElement('h1', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 17}}, _foobar2.default)
), container);

I'm curious about what __self and __source are used for.

Integrate latest babel-parser changes

It looks like babel-parser now supports new TypeScript 2.9 syntax and has some bug fixes, so it would be good to pull in those changes. It's not quite up-to-date with TS 3.0 syntax, but hopefully will be at some point.

prepend _interopRequireWildcard once you know an import exists

_interopRequireWildcard and _interopRequireDefault are included regardless of whether an import statement exists in the source. I would like to avoid this.

Also, I would like the option to exclude those functions entirely, because I will include them manually after concatenating sources.

Make React development mode an option

transform('<a/>', {transforms: ['jsx']});

generates code that has __self and __source props. This is normally only enabled when you use preset-react with development to true.

TODO: Cannot have generic args on object field values in TypeScript

// src/foo.ts
export const Foo = {
    bar<T>() {
        return;
    }
};
$ sucrase src -d out --transforms typescript
src/foo.ts -> out/foo.js
Error: Error transforming src/foo.ts: TODO
    at tsStartParseObjPropValue (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/plugins/typescript.js:1258:11)
    at parseObjPropValue (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:833:46)
    at parseObj (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:756:5)
    at parseExprAtom (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:487:7)
    at parseExprSubscripts (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:258:20)
    at parseMaybeUnary (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:244:20)
    at parseExprOps (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:184:20)
    at parseMaybeConditional (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:157:20)
    at baseParseMaybeAssign (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/traverser/expression.js:141:20)
    at tsParseMaybeAssign (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/parser/plugins/typescript.js:1306:49)

Link: https://sucrase.io/#code=export%20const%20Foo%20%3D%20%7B%0A%20%20%20%20bar%3CT%3E()%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%7D%3B%0A

Curiously, Sucrase behavior is compatible with your sample's Babel stack. โ˜บ๏ธ

What about react HMR

Sucrase looks promising for a faster dev experience. But what about support form HMR with react?
The popular solution is to use react-hot-loader but it's a babel plugin... Any existing solution to support this feature with Sucrase?

Improve performance by compiling to webassembly

I was able to prototype a hacked-together variant of the sucrase-babylon parser and get it working in AssemblyScript. I ran it through a benchmark of compiling a ~100 line file over and over and counting the number of tokens, and it gave the correct answer both when run from js and when run from wasm. Here were the results:

# iterations   JS time taken    wasm time taken
1              10.065ms         0.192ms
10             19.173ms         1.004ms
100            72.770ms         9.074ms
1000           182.896ms        72.428ms
10000          594.523ms        899.885ms
100000         4180.687ms       8830.265ms

As expected, the wasm running time grows linearly and the JS running time grows sub-linearly at least in small numbers, since V8 has more and more time to optimize the code and wasm optimizations are all at compile time. The wasm code is 50x faster on a very small case, about equal for about 50,000 lines of code, and 2x slower in the extremes. So it looks like, at least for now, V8 can do a really good job at optimization when given long enough, enough to outperform the equivalent code compiled through AssemblyScript.

Sadly, this means webassembly isn't a slam dunk like I was hoping, but it still seems promising. Probably there's room for improvement in the use of webassembly (tricks that would be impossible to implement in plain JS and unreasonable for V8 to infer), and AssemblyScript and WebAssembly both may improve over time, so there's reason to believe webassembly will eventually win out for long-running programs after all. This test case may also be beneficial for V8 since it's processing exactly the same dataset, so the branches and call statistics will be the same every time. This also only handles about half of the sucrase codebase, and the other half (determining and performing the string replacements) may run better in wasm compared with JS.

My plan now is to get Sucrase into a state where it can both run in JS and be compiled by AssemblyScript to webassembly. That should at least make it a good test bed for this stuff.

Another good thing to prototype would be porting the parser to Rust and/or C++ and see how it performs as native code vs wasm, and see how that wasm compares with the wasm produced by AssemblyScript.

Missing babylon license

I understand that sucrase-babylon is a fork, but you still need to include the LICENSE and AUTHORS file.

Plugins

Hey Alan, this is a really cool project. Thanks for your hard work!

As you know, a large part of Babel's appeal is its extensibility. Although sucrase avoids that bucket of worms (and for good reason), its users can still harness the power of plugins by using nebu after sucrase. Nebu is still very young, but I think these two projects will complement each other greatly. It uses magic-string (the same library that Bublรฉ uses) to skip the AST-to-code generation phase in favor of simple string splits/joins (but with sourcemap support).

Cheers!

Project roadmap and ways others can contribute

I've had a few requests from others about how they can help out. I've been keeping a checklist of possible future tasks, and figured it would be good to make that public to make it easier for others to contribute.

Feel free to stop by and chat in the gitter room, which I just created. Generally my thought is that this can be a big checklist of possible things to do, and as work starts on them, they can be "promoted" to real issues with individual discussions/assignees/etc. I'm sure there's plenty of context to be shared about most of these issues, so feel free to chat if you want to take one of them on.

Currently, I'm focused on performance, and want to see how high I can get the N in "Sucrase is N times faster than Babel". But there's certainly lots of other stuff to do as well.

Stability

  • Add additional example projects that can be automatically tested. Ideally they would be large codebases in TypeScript/Flow/JSX and have fast test suites so they can be run as part of PR builds.
  • Find a way to integrate the Babylon test suite (and maybe other test suites like the TypeScript test suite and test262). I worry that there are some subtle edge cases around arrow function parsing that are missed right now.
  • Set up istanbul and make sure the tests have good code coverage.

Bugs

  • Complex assignments to mutable exports aren't handled correctly, e.g. export let x; [x] = arr;. Babel and TypeScript are both incomplete here, and ideally we just mimic one or the other.
  • TypeScript import pruning does not yet handle shadowed imported names.
  • import statements should be hoisted to the top of the file, but they aren't right now.
  • The line numbers almost always match the source line numbers, but that doesn't happen correctly for multiline class field initializers.
  • There are subtle differences between Babel and TypeScript around the transformed result of import and export. Ideally we exactly copy either Babel or TypeScript, based on the config.
  • Fix the disabled tests, which both crash the parser for TypeScript.
  • Class field computed names with side-effects should be evaluated only once instead of on each constructor invocation.
  • react-display-name has a case where it uses the filename to determine the display name for an anonymous export default React class. We could add a filename arg to the Sucrase API to support this, although it's maybe obscure enough that the API change isn't worth it.

Tooling

  • Figure out automated releases. semantic-release might work, but it's less clear since it's a monorepo (each integration is its own package).
  • Auto-publish the website on every release.
  • Set up Greenkeeper or similar.
  • See if Lerna would be useful. At the moment, the only packages other than the main sucrase package are small integrations, so Lerna may make less sense than for a larger project like Babel.
  • Enable noImplicitThis in the TypeScript config. Implicit any for this has bit me a few times, but there are enough pre-existing errors that it's hard to enable.
  • Figure out a good way for Sucrase to compile itself for production builds. At the moment, there's a self-build task, but we still use TypeScript for the real build.

Website (sucrase.io)

  • It would be sweet to have a "file a bug" button that you can click to automatically submit a bug report with the current code. Even better if it can notice that Sucrase crashed and Babel succeeded, or Sucrase produced invalid JS, and explicitly prompt you to file a bug.
  • Port the website to TypeScript or Flow and use Sucrase to build it.
  • I'm sure there are plenty of visual polish improvements that could be made.
  • Better handle Sucrase getting into an infinite loop, e.g. by adding a generous timeout (maybe 10 seconds) and restarting the web worker and giving a friendly error message.
  • Improve the initial load experience. The Monaco editor is a fairly large download, and the web worker is also huge (1.1MB compressed), possibly due to babel-standalone being big. One approach is to optimize the bundle size, and another approach is to degrade better, e.g. showing a dumb editor if Monaco isn't loaded and only running Sucrase if Babel isn't loaded.
  • Make the website more mobile-friendly.
  • I recently disabled service workers because they were causing cache issues, but it would be nice to re-enable them.

Profiling/benchmarking

  • Find a way to make performance numbers more stable, e.g. a dedicated server with nothing else running.
  • Come up with a more realistic benchmark to test on. The current one is a bit boring/repetitive and may not be representative of real code.
  • Explore other profiling techniques, like figuring out a way to measure the cost of function call overhead or other overhead, or looking at any JIT diagnostics provided by V8.
  • It would be awesome to have a part of the website showing historical performance data, similar to https://arewefastyet.com/ . For example, it could run the Babel and Sucrase build on every example project every day and show how they compare.

Features

  • Figure out if we need source maps, and implement them if so. The output line numbers are the same as the input line numbers, so theoretically they shouldn't be necessary at least for things like stack traces. (Spun off as #223.)
  • Support the TypeScript and Flow transforms with the imports transform disabled. (In other words, targeting ES modules).
  • Add a transform for object rest/spread. I was hoping to avoid this, but it looks like Webpack uses a parser that doesn't support it, so it may be necessary to implement for practical reasons. Also, MDN says it's not supported at all in Edge or Safari. (Note that webpack-object-rest-spread-plugin makes this work in Webpack, and object will probably be very hard to implement in nested cases, so probably this transform should be called out of scope.) I'm calling this out of scope.

Configuration

  • Allow Sucrase to read a tsconfig.json for its config (including which directories to compile and which to exclude, etc).
  • Allow Sucrase to read a .babelrc for its config.
  • Think more about how to simplify the configuration. One alternate approach is to specify the language being compiled (TS, TS+JSX, Flow, Flow+JSX, JSX) and the target (ESM, CJS) rather than a list of transforms. Much of this can be inferred from context, though, so ideally in most situations there can be zero configuration.
  • Another idea I've had is "Sucrase Surgeon", which goes into your node_modules directory and patches Babel/TypeScript to run Sucrase instead. It would be fragile, but it would let you try out Sucrase without needing to make any changes to your build config. It wouldn't make sense for real use, but it could lower the barrier to entry.

Integrations

  • Improve the current integrations (gulp, jest, require hook), e.g. providing better ways to specify options.
  • Add a Webpack loader.
  • Add a Rollup plugin.
  • Add a node ES module loader hook so it's possible to transform modules on-the-fly using native ES module support in node.
  • Any other integrations that seem useful.
  • See if there's a nicer way to share the build system across projects and manage publishing and versions.

Performance

  • Prototype a part of the project (probably the lexer) in AssemblyScript.
  • Prototype a part of the project in Rust, both compiled to WebAssembly and run natively.
  • See how practical it would be to write a custom WebAssembly code generator.
  • Further simplify the parser by removing code that we know isn't needed.
  • Dumb down all of the code so that it's more reasonable to run in WebAssembly (no classes or inheritance, no dynamic objects, etc.).
  • Find ways to skip unnecessary parsing details, e.g. skipping all tokens in curly braces when parsing a type.
  • Avoid backtracking in the parser, or find a way to make it more performant.
  • Consider using lower-level features, e.g. ints instead of strings to identify tokens, and mutating an array rather than building a string using +.
  • Consider simplifying the token format, e.g. making it so each type is just one full token.

Inferred function names don't work with "export let" statements

This code:

export const f = () => {}

becomes this with the imports transform:

exports.f = () => {}

Playground link

This is mostly correct, but stops JS from inferring the name as 'f' from the syntactic position, as described in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#Inferred_function_names . tsc has the same issue, but Babel transpiles in a way that respects the inferred name, so Sucrase should as well.

Add support for JSX fragments

Example code:

const e = (
  <>
    <div />
    <span />
  </>
);

Playground link

Similar to #207, this should be configurable. Like the Babel plugin (docs), the JSX element should be called React.Fragment by default and configurable, probably as jsxFragmentPragma.

Does not support properties that must be quoted

I was trying to use sucrase to transform a library that makes use of kebab-case props (e.g. aria-hidden). Sucrase produces invalid JavaScript when trying to transform that property.

Example source:

<Foo aria-hidden />

Sucrase output:

"use strict";const _jsxFileName = "sample.tsx";React.createElement(Foo, { aria-hidden: true, __self: this, __source: {fileName: _jsxFileName, lineNumber: 1}} )

The invalid output is the unquoted object property since aria-hidden is not a valid identifier.

For reference, here is the Babel output:

"use strict";

var _jsxFileName = "sample.tsx";
React.createElement(Foo, {
  "aria-hidden": true,
  __source: {
    fileName: _jsxFileName,
    lineNumber: 1
  },
  __self: void 0
});

Multiline class fields break line number preservation

Currently, class fields become assignments in the constructor. The number of lines of the class field declaration is preserved, but the constructor start might have newlines, causing duplicate newlines and more overall lines in the output than the input. In addition, this makes it impossible to set a breakpoint for bound handles methods e.g. in React components, since the method body moves to the constructor.

For example, this:

class Foo extends React.Component {
  componentDidMount() {
    console.log('Mounted!');
  }
  handleClick = () => {
    console.log('Clicked!');
  }
  render() {
    return <span onClick={this.handleClick} />;
  }
}

Becomes this:

const _jsxFileName = "sample.js";
class Foo extends React.Component {constructor(...args) { super(...args); this.handleClick = () => {
    console.log('Clicked!');
  }; }
  componentDidMount() {
    console.log('Mounted!');
  }
  


  render() {
    return React.createElement('span', { onClick: this.handleClick, __self: this, __source: {fileName: _jsxFileName, lineNumber: 10}} );
  }
}

Playground link

I think a better approach, which isn't quite as correct but helps the dev experience significantly, is to do something like this:

const _jsxFileName = "sample.js";
class Foo extends React.Component {constructor(...args) { super(...args); this.__setField1(); }
  componentDidMount() {
    console.log('Mounted!');
  }
  __setField1() {this.handleClick = () => {
    console.log('Clicked!');
  };}
  render() {
    return React.createElement('span', { onClick: this.handleClick, __self: this, __source: {fileName: _jsxFileName, lineNumber: 10}} );
  }
}

That way breakpoints will work on the "Clicked!" line and every line below it without complicating source maps.

Design thoughts:

  • Class methods are already non-enumerable, so adding new methods should generally not cause problems in existing code.
  • Rather than generating an unused name, we could create a symbol, but it seems like that probably shouldn't matter much here, since we can always make a name that doesn't match any identifier in the file.
  • Static fields would be similar, something like static __setStaticField1() { this.displayName = 'Foo'; } with Foo.__setStaticField1() after the class (which we already do).

Removing extension from "main" in Package.json for ESM/CJS interop

"main": "dist/index.js",

Currently Node (with --experimental-modules) can decide to either load '.mjs' or '.js' depending on the mode (ie importing into ES or importing/requiring into cjs) however this only works if the "main" in package.json does not specify an explicit extension.

{
  "main": "dist/index"
}

To the best of my knowledge, omitting the extension should not have any side effects in earlier versions of Node, but it is worth verifying on your end if you are willing to consider making this minor change.

Thanks

TypeScript import pruning does not respect shadowed usages

This code in the TypeScript transform should NOT import the ./a module since it's not used (meaning "it's only used as a type" is vacuously true).

import a from './a';

function foo() {
  let a;
  a();
}

Instead, Sucrase gives this output:

"use strict"; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _a = require('./a'); var _a2 = _interopRequireDefault(_a);

function foo() {
  let a;
  a();
}

Playground link

Sucrase should notice that the a() usage refers to the shadowed a declaration and not to the import, and thus not count it as a runtime usage of a.

webpack-loader with object rest spread plugin fails parsing

When trying to run sucrase with the webpack-loader and webpack-object-rest-spread-plugin I get the following error:

ERROR in ./src/Main.jsx
Module parse failed: this.applyPluginsBailResult is not a function
You may need an appropriate loader to handle this file type.
TypeError: this.applyPluginsBailResult is not a function

Versions used:

webpack: 4.8.3
sucrase: 2.2.0
@sucrase/webpack-loader: 1.0.2,
@sucrase/webpack-object-rest-spread-plugin: 1.0.0

Semicolons in class bodies seem to cause problems with webpack

Example:

class A {
  b = 1;
}

becomes this:

"use strict";class A {constructor() { this.b = 1; }
  ;
}

Playground link

This behavior changed in Sucrase 3.2.0 ( #271 ), and it caused Sucrase 3.2.1 to be compiled in a way that broke the webpack build in the sucrase.io website. For some reason, it causes webpack 3.5.1 to silently stop about 15 seconds in with no error message. For now, it seems best to change the behavior to avoid ever emitting a lone semicolon.

"Quiet" mode

It'd be nice to have a "quiet" mode like -q in the babel cli where the print statements during sucrase' compilation are not displayed in the terminal.

Typescript transpile SyntaxError: A trailing comma is not permitted after the rest element

It's forbidden in ES, but allowed in TS, so prettier inserts the comma, see prettier/prettier#3586 and prettier/prettier#3528.

Happens with latest [email protected].

ERROR in ./src/components/X.tsx
Module build failed (from ../node_modules/@sucrase/webpack-loader/dist/index.js):
SyntaxError: Error transforming ./src/components/X.tsx: A trailing comma is not permitted after the rest element (1:1032)
    at raise (./node_modules/sucrase/dist/parser/traverser/base.js:24:15)
    at unexpected (./node_modules/sucrase/dist/parser/traverser/util.js:63:25)
    at parseObj (./node_modules/sucrase/dist/parser/traverser/expression.js:719:32)
    at parseBindingAtom (./node_modules/sucrase/dist/parser/traverser/lval.js:63:32)
    at parseMaybeDefault (./node_modules/sucrase/dist/parser/traverser/lval.js:131:5)
    at parseAssignableListItem (./node_modules/sucrase/dist/parser/traverser/lval.js:115:3)
    at parseBindingList (./node_modules/sucrase/dist/parser/traverser/lval.js:104:7)
    at parseFunctionParams (./node_modules/sucrase/dist/parser/traverser/statement.js:579:30)
    at parseParenAndDistinguishExpression (./node_modules/sucrase/dist/parser/traverser/expression.js:608:42)
    at parseExprAtom (./node_modules/sucrase/dist/parser/traverser/expression.js:477:24)
    at parseExprSubscripts (./node_modules/sucrase/dist/parser/traverser/expression.js:258:20)
    at parseMaybeUnary (./node_modules/sucrase/dist/parser/traverser/expression.js:244:20)
    at parseExprOps (./node_modules/sucrase/dist/parser/traverser/expression.js:184:20)
    at parseMaybeConditional (./node_modules/sucrase/dist/parser/traverser/expression.js:157:20)
    at baseParseMaybeAssign (./node_modules/sucrase/dist/parser/traverser/expression.js:141:20)
    at tsParseMaybeAssign (./node_modules/sucrase/dist/parser/plugins/typescript.js:1306:49)

jest-plugin breaks tests using jest.mock("./path/to/module.js");

At the start of my test file I have:

jest.mock("../util/active-tracker.js");

Inside the tests is:

        const {
            default: ActiveTracker,
        } = await import("../util/active-tracker.js");

        const mockTracker = ActiveTracker.mock.instances[0];
        mockTracker.steal.mockClear();

jest runs into an exception on the last line because mock.instances is empty.

My jest config:

  "jest": {
    "globals": {
      "SNAPSHOT_INLINE_APHRODITE": true
    },
    "testRegex": ".test\\.jsx?$",
    "setupTestFrameworkScriptFile": "./test-setup.js",
    "transform": {
      "^.+\\.jsx?$": "@sucrase/jest-plugin"
    },
    "collectCoverageFrom": [
      "packages/**/*.js",
      "!packages/**/dist/index.js",
      "!<rootDir>/node_modules/",
      "!packages/**/node_modules/"
    ]
  },

Fails to import commonjs module without default export when transpiling typescript

Example module [email protected], see JedWatson/classnames#105.

In typescript, we use import * as cn from 'classnames' to import from module without default export.

This gets transpiled using babel's _interopRequireDefault to

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _classnames = __webpack_require__(/*! classnames */ "../node_modules/classnames/index.js");
var _classnames2 = _interopRequireDefault(_classnames);

resulting in cn equal to { default: classNames() } instead of classNames() function, and throwing Uncaught TypeError: cn is not a function.

Is there any way to fix it and handle such exports correctly?

Remove Flow pragmas

It is possible to remove // @flow and /* @flow */ pragmas when transform flow is enabled. Such pragmas is a part of Flow and hence should be removed.

It's difficult, but it would be absolute awesome if Flow's suppress_comment (.flowconfig) pragmas for current project are also removed as well.

Does not catch mis-matching react tags.

Sucrase ignores malformed JSX input such as

<div>
</vid>

whereas Babel would catch this as an error. This could lead to some weird parsing and hard to find human error whilst developing.

Add code coverage in CI

Given that part of the goal of this project is to have a small amount of highly-reliable code, seems like having really good coverage and enforcing it is a good way to showcase that and keep the project honest. Current stats from running in WebStorm show 82% coverage, so seems like there could be some improvement. Seems like https://coveralls.io/ might be the best way to do that.

Yarn warning: "@sucrase/[email protected]" has incorrect peer dependency "sucrase@^1.12.1"

[email protected] reports the following warning when using latest sucrase@^2.2.0:

"@sucrase/[email protected]" has incorrect peer dependency "sucrase@^1.12.1"

Maybe allowing both versions ^1.12.1 and ^2 in package.json would help. The range can be specified either as ^1.12.1 || ^2, or as 1.12.1 - 2, try it at the semver check:

  "peerDependencies": {
    "sucrase": "1.12.1 - 2"
  },
  "devDependencies": {
    "@types/loader-utils": "^1.1.1",
    "sucrase": "1.12.1 - 2"
  },

Class Fields

When attempting to test out the transpiling via the online tool, I found that it would throw an error when attempting to process class properties; Expected rhsEndIndex on class field assignment.

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Paginate extends Component {
  static propTypes = {
    count: PropTypes.number.isRequired
  }

  render () {
    const {
      props: { count, size }
    } = this;

    return (
      <section className='paginate'>
        <div>{count} total row(s)</div>
      </section>
    );
  }
}

Suggestion: drop dependency on React.createElement

Not sure if you're aware, but JSX is used for other projects too, such as virtual-dom, snabbdom, surplus and others. It would definitely be worthwhile making the element factory function configurable, rather than hardcoding it to React.createElement.

Cannot have index signature type decl in TypeScript class

Sucrase chokes on the following input, where tsc does not:

// src/foo.ts
export class Foo {
    [name: string]: any;
}
$ sucrase src -d out --transforms typescript
src/foo.ts -> out/foo.js
Error: Error transforming src/foo.ts: Expected class context ID on computed name open bracket.
    at getNameCode (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/util/getClassInfo.js:232:13)
    at getClassInfo (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/util/getClassInfo.js:69:24)
    at RootTransformer.processClass (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/transformers/RootTransformer.js:154:50)
    at RootTransformer.processToken (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/transformers/RootTransformer.js:129:12)
    at RootTransformer.processBalancedCode (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/transformers/RootTransformer.js:123:12)
    at RootTransformer.transform (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/transformers/RootTransformer.js:78:10)
    at transform (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/index.js:47:25)
    at buildFile (/Users/alexr/.config/yarn/global/node_modules/sucrase/dist/cli.js:84:48)
    at <anonymous>

Properly handle evaluation order issues between imports and other code

This code:

console.log('foo');
import './bar';

gets compiled to this code in Sucrase:

"use strict";
console.log('foo');
require('./bar');

This matches the tsc output, but disagrees with the Babel output:

"use strict";

require("./bar");

console.log('foo');

Playground link

Babel is more correct in that imports are always evaluated first, regardless of their position in the file. Still, it's unclear what Sucrase should do here. Some thoughts:

  • For TypeScript in particular, it's ambiguous what should be done, since TypeScript code will behave differently under Babel and tsc. That also indicates that it's probably not as important as other correctness issues, and implementing it could actually break usages that are relying on the tsc behavior.
  • Arguably it's sloppy code to ever have imports anywhere but the top of the file, and this can already be checked with the import/first ESLint rule: https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/first.md
  • It's sort of a pain for Sucrase to implement this properly while also preserving line numbers. Sucrase so far hasn't had to do much rearranging of code.
  • From a certain idealistic viewpoint, the import transform is just temporary anyway, and making it precisely correct isn't as important for the project in the long term.

My current thinking is that Sucrase won't implement the reordering, but there should be a place for this nuance to be documented. I've also had thoughts of making an ESLint config/rule/plugin that flags any possible issues like this, so this would certainly be a good candidate for that. But if anyone feels that the reordering transform would be particularly useful for them, feel free to comment here.

Comments within types have all whitespace removed

This is more of a cosmetic issue, but certainly looks weird.

This code:

interface A {
  // This is a comment.
}

Becomes this code:

"use strict";
//Thisisacomment.

https://sucrase.io/#code=interface%20A%20%7B%0A%20%20%2F%2F%20This%20is%20a%20comment.%0A%7D%0A

This happens because we remove all non-newline whitespace between tokens in some situations. It looks like it may be best to either keep all whitespace or remove all non-newline characters (assuming the only code between tokens is comments and that comments are always safe to remove).

Generate source maps

Mentioned by @Rich-Harris in #221.

Currently, Sucrase preserves line numbers, which gets many of the benefits of source maps (like reliable stack traces when using sucrase/register), but ideally it would emit real source maps. If we just care about lines, I think it would be a trivial source map that maps each line to itself. Mapping tokens within each line (which I think is useful for debuggers) should also be doable, but would need to be more integrated into the code generator.

ES5 stand alone build

We have an archaic build system that uses Nashorn (part of Java) to compile our JSX to to JS and then after that we run Closure Compiler on it. We initially started off using JSXTransformer and then migrated to babel for our jsx transformer. Babel is super slow (as you know). I'm trying to replace Babel with Sucrase as the jsx to js transformer and it was working fine up until 3.x. The code for this is at https://github.com/arv/sucrase-standalone.

Would you be interested in having an es5 stand alone build as part of the official sucrase package?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.