Skip to content

Commit

Permalink
Add support for custom JSX pragmas
Browse files Browse the repository at this point in the history
Fixes #207

As with Babel, we assume the pragma is a dot-separated list of identifiers (or
just an identifier), which we parse so that we can replace the base name if
necessary.
  • Loading branch information
alangpierce committed May 20, 2018
1 parent 4c55912 commit 1ec646e
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 20 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ The following proposed JS features are built-in and always transformed:
* [Optional catch binding](https://github.com/tc39/proposal-optional-catch-binding):
`try { doThing(); } catch { }`.

When using the `import` transform, there are some options to enable legacy
CommonJS interop approaches:
### JSX Options
Like Babel, Sucrase compiles JSX to React functions by default, but can be
configured for any JSX use case.
* **jsxPragma**: Element creation function, defaults to `React.createElement`.
* **jsxFragmentPragma**: Fragment component, defaults to `React.Fragment`.

### Legacy CommonJS interop
Two legacy modes can be used with the `import` tranform:
* **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
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type Transform = "jsx" | "typescript" | "flow" | "imports";

export type Options = {
transforms: Array<Transform>;
jsxPragma?: string;
jsxFragmentPragma?: string;
enableLegacyTypeScriptModuleInterop?: boolean;
enableLegacyBabel5ModuleInterop?: boolean;
filePath?: string;
Expand All @@ -35,7 +37,7 @@ export function transform(code: string, options: Options): string {
sucraseContext,
options.transforms,
Boolean(options.enableLegacyBabel5ModuleInterop),
options.filePath || null,
options,
).transform();
} catch (e) {
if (options.filePath) {
Expand Down
40 changes: 33 additions & 7 deletions src/transformers/JSXTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import XHTMLEntities from "../../sucrase-babylon/plugins/jsx/xhtml";
import {TokenType as tt} from "../../sucrase-babylon/tokenizer/types";
import CJSImportProcessor from "../CJSImportProcessor";
import {Options} from "../index";
import NameManager from "../NameManager";
import TokenProcessor from "../TokenProcessor";
import RootTransformer from "./RootTransformer";
Expand All @@ -13,15 +14,33 @@ export default class JSXTransformer extends Transformer {
lastLineNumber: number = 1;
lastIndex: number = 0;
filenameVarName: string | null = null;
readonly jsxPragmaBase: string;
readonly jsxPragmaSuffix: string;
readonly jsxFragmentPragmaBase: string;
readonly jsxFragmentPragmaSuffix: string;

constructor(
readonly rootTransformer: RootTransformer,
readonly tokens: TokenProcessor,
readonly importProcessor: CJSImportProcessor | null,
readonly nameManager: NameManager,
readonly filePath: string | null,
readonly options: Options,
) {
super();
[this.jsxPragmaBase, this.jsxPragmaSuffix] = this.splitPragma(
options.jsxPragma || "React.createElement",
);
[this.jsxFragmentPragmaBase, this.jsxFragmentPragmaSuffix] = this.splitPragma(
options.jsxFragmentPragma || "React.Fragment",
);
}

private splitPragma(pragma: string): [string, string] {
let dotIndex = pragma.indexOf(".");
if (dotIndex === -1) {
dotIndex = pragma.length;
}
return [pragma.slice(0, dotIndex), pragma.slice(dotIndex)];
}

process(): boolean {
Expand All @@ -34,7 +53,7 @@ export default class JSXTransformer extends Transformer {

getPrefixCode(): string {
if (this.filenameVarName) {
return `const ${this.filenameVarName} = ${JSON.stringify(this.filePath || "")};`;
return `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`;
} else {
return "";
}
Expand Down Expand Up @@ -181,16 +200,23 @@ export default class JSXTransformer extends Transformer {
}

processJSXTag(): void {
const resolvedReactName = this.importProcessor
? this.importProcessor.getIdentifierReplacement("React") || "React"
: "React";
const {jsxPragmaBase, jsxPragmaSuffix, jsxFragmentPragmaBase, jsxFragmentPragmaSuffix} = this;
const resolvedPragmaBaseName = this.importProcessor
? this.importProcessor.getIdentifierReplacement(jsxPragmaBase) || jsxPragmaBase
: jsxPragmaBase;
const firstTokenStart = this.tokens.currentToken().start;
// First tag is always jsxTagStart.
this.tokens.replaceToken(`${resolvedReactName}.createElement(`);
this.tokens.replaceToken(`${resolvedPragmaBaseName}${jsxPragmaSuffix}(`);

if (this.tokens.matches1(tt.jsxTagEnd)) {
// Fragment syntax.
this.tokens.replaceToken(`${resolvedReactName}.Fragment, null, `);
const resolvedFragmentPragmaBaseName = this.importProcessor
? this.importProcessor.getIdentifierReplacement(jsxFragmentPragmaBase) ||
jsxFragmentPragmaBase
: jsxFragmentPragmaBase;
this.tokens.replaceToken(
`${resolvedFragmentPragmaBaseName}${jsxFragmentPragmaSuffix}, null, `,
);
// Tag with children.
this.processChildren();
while (!this.tokens.matches1(tt.jsxTagEnd)) {
Expand Down
5 changes: 3 additions & 2 deletions src/transformers/ReactDisplayNameTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ContextualKeyword, IdentifierRole} from "../../sucrase-babylon/tokenizer";
import {TokenType as tt} from "../../sucrase-babylon/tokenizer/types";
import CJSImportProcessor from "../CJSImportProcessor";
import {Options} from "../index";
import TokenProcessor from "../TokenProcessor";
import RootTransformer from "./RootTransformer";
import Transformer from "./Transformer";
Expand All @@ -18,7 +19,7 @@ export default class ReactDisplayNameTransformer extends Transformer {
readonly rootTransformer: RootTransformer,
readonly tokens: TokenProcessor,
readonly importProcessor: CJSImportProcessor | null,
readonly filePath: string | null,
readonly options: Options,
) {
super();
}
Expand Down Expand Up @@ -104,7 +105,7 @@ export default class ReactDisplayNameTransformer extends Transformer {
}

private getDisplayNameFromFilename(): string {
const filePath = this.filePath || "unknown";
const filePath = this.options.filePath || "unknown";
const pathSegments = filePath.split("/");
const filename = pathSegments[pathSegments.length - 1];
const dotIndex = filename.lastIndexOf(".");
Expand Down
8 changes: 4 additions & 4 deletions src/transformers/RootTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TokenType as tt} from "../../sucrase-babylon/tokenizer/types";
import {SucraseContext, Transform} from "../index";
import {Options, SucraseContext, Transform} from "../index";
import NameManager from "../NameManager";
import TokenProcessor from "../TokenProcessor";
import getClassInfo, {ClassInfo} from "../util/getClassInfo";
Expand All @@ -24,7 +24,7 @@ export default class RootTransformer {
sucraseContext: SucraseContext,
transforms: Array<Transform>,
enableLegacyBabel5ModuleInterop: boolean,
filePath: string | null,
options: Options,
) {
this.nameManager = sucraseContext.nameManager;
const {tokenProcessor, importProcessor} = sucraseContext;
Expand All @@ -35,10 +35,10 @@ export default class RootTransformer {
this.transformers.push(new OptionalCatchBindingTransformer(tokenProcessor, this.nameManager));
if (transforms.includes("jsx")) {
this.transformers.push(
new JSXTransformer(this, tokenProcessor, importProcessor, this.nameManager, filePath),
new JSXTransformer(this, tokenProcessor, importProcessor, this.nameManager, options),
);
this.transformers.push(
new ReactDisplayNameTransformer(this, tokenProcessor, importProcessor, filePath),
new ReactDisplayNameTransformer(this, tokenProcessor, importProcessor, options),
);
}

Expand Down
87 changes: 83 additions & 4 deletions test/jsx-test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import {JSX_PREFIX} from "./prefixes";
import {Transform} from "../src";
import {IMPORT_PREFIX, JSX_PREFIX} from "./prefixes";
import * as util from "./util";

const {devProps} = util;

function assertResult(code: string, expectedResult: string): void {
util.assertResult(code, expectedResult, {transforms: ["jsx"]});
util.assertResult(code, expectedResult, {transforms: ["jsx", "flow"]});
function assertResult(
code: string,
expectedResult: string,
{
extraTransforms,
jsxPragma,
jsxFragmentPragma,
}: {extraTransforms?: Array<Transform>; jsxPragma?: string; jsxFragmentPragma?: string} = {},
): void {
const transforms: Array<Transform> = ["jsx", ...(extraTransforms || [])];
util.assertResult(code, expectedResult, {transforms, jsxPragma, jsxFragmentPragma});
}

describe("transform JSX", () => {
Expand Down Expand Up @@ -310,4 +319,74 @@ describe("transform JSX", () => {
`,
);
});

it("handles transformed react name in createElement and Fragment", () => {
assertResult(
`
import React from 'react';
const f = (
<>
<div />
<span />
</>
);
`,
`"use strict";${JSX_PREFIX}${IMPORT_PREFIX}
var _react = require('react'); var _react2 = _interopRequireDefault(_react);
const f = (
_react2.default.createElement(_react2.default.Fragment, null,
, _react2.default.createElement('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 5}} )
, _react2.default.createElement('span', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 6}} )
)
);
`,
{extraTransforms: ["imports"]},
);
});

it("allows custom JSX pragmas", () => {
assertResult(
`
const f = (
<>
<div />
<span />
</>
);
`,
`${JSX_PREFIX}
const f = (
h(Fragment, null,
, h('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} )
, h('span', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 5}} )
)
);
`,
{jsxPragma: "h", jsxFragmentPragma: "Fragment"},
);
});

it("properly transforms imports for JSX pragmas", () => {
assertResult(
`
import {h, Fragment} from 'preact';
const f = (
<>
<div />
<span />
</>
);
`,
`"use strict";${JSX_PREFIX}${IMPORT_PREFIX}
var _preact = require('preact');
const f = (
_preact.h(_preact.Fragment, null,
, _preact.h('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 5}} )
, _preact.h('span', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 6}} )
)
);
`,
{extraTransforms: ["imports"], jsxPragma: "h", jsxFragmentPragma: "Fragment"},
);
});
});

0 comments on commit 1ec646e

Please sign in to comment.