Skip to content

Commit 92ff4f0

Browse files
Merge pull request #344 from FormidableLabs/feature/typescript-support
Add first-class support for `inline` and `render` TypeScript components
2 parents 10ce554 + d561f07 commit 92ff4f0

File tree

12 files changed

+180
-19
lines changed

12 files changed

+180
-19
lines changed

.eslintrc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
{
22
"root": true,
33
"parser": "@babel/eslint-parser",
4-
"plugins": [
5-
"prettier"
6-
],
4+
"plugins": ["prettier"],
5+
"settings": {
6+
"react": {
7+
"version": "detect"
8+
}
9+
},
710
"extends": [
811
"eslint:recommended",
912
"plugin:react/recommended",

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ It supports these props, while passing any others through to the `children`:
1616
| noInline | `PropTypes.bool` | Doesn’t evaluate and mount the inline code (Default: `false`). Note: when using `noInline` whatever code you write must be a single expression (function, class component or some `jsx`) that can be returned immediately. If you'd like to render multiple components, use `noInline={true}` |
1717
| transformCode | `PropTypes.func` | Accepts and returns the code to be transpiled, affording an opportunity to first transform it |
1818
| language | `PropTypes.string` | What language you're writing for correct syntax highlighting. (Default: `jsx`) |
19+
| enableTypeScript | `PropTypes.bool` | Enables TypeScript support in transpilation. (Default: `true`) |
1920
| disabled | `PropTypes.bool` | Disable editing on the `<LiveEditor />` (Default: `false`) |
2021
| theme | `PropTypes.object` | A `prism-react-renderer` theme object. See more [here](https://github.com/FormidableLabs/prism-react-renderer#theming) |
2122

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
"prepublishOnly": "npm run build",
1717
"test": "jest",
1818
"test:typings": "typings-tester --dir typings",
19-
"lint": "eslint --config .eslintrc \"./**/*.js\"",
19+
"lint": "eslint --config .eslintrc \"./src/**/*.js\"",
2020
"install:docs": "yarn --cwd website install",
2121
"start:docs": "yarn --cwd website start",
2222
"build:docs": "yarn --cwd website build",
2323
"format:docs": "prettier --write docs"
2424
},
2525
"dependencies": {
2626
"prism-react-renderer": "^1.3.1",
27-
"sucrase": "^3.21.0",
27+
"sucrase": "^3.31.0",
2828
"use-editable": "^2.3.3"
2929
},
3030
"devDependencies": {

src/components/Live/LivePreview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useContext } from "react";
22
import PropTypes from "prop-types";
33
import LiveContext from "./LiveContext";
44

5-
function LivePreview({ Component, ...rest }) {
5+
function LivePreview({ Component = "div", ...rest }) {
66
const { element: Element } = useContext(LiveContext);
77
return <Component {...rest}>{Element ? <Element /> : null}</Component>;
88
}

src/components/Live/LiveProvider.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ function LiveProvider({
99
code,
1010
language,
1111
theme,
12+
enableTypeScript = true,
1213
disabled,
1314
scope,
1415
transformCode,
@@ -42,6 +43,7 @@ function LiveProvider({
4243
const input = {
4344
code: transformedCode,
4445
scope,
46+
enableTypeScript,
4547
};
4648

4749
if (noInline) {
@@ -89,6 +91,7 @@ LiveProvider.propTypes = {
8991
children: PropTypes.node,
9092
code: PropTypes.string,
9193
disabled: PropTypes.bool,
94+
enableTypeScript: PropTypes.bool,
9295
language: PropTypes.string,
9396
noInline: PropTypes.bool,
9497
scope: PropTypes.object,
@@ -98,6 +101,7 @@ LiveProvider.propTypes = {
98101

99102
LiveProvider.defaultProps = {
100103
code: "",
104+
enableTypeScript: true,
101105
noInline: false,
102106
language: "jsx",
103107
disabled: false,

src/utils/test/compose.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import compose from "../transpile/compose";
2+
3+
describe("compose", () => {
4+
it("should compose functions and call the result", () => {
5+
const curriedAdd = (a) => (b) => b + a;
6+
const curriedSubtract = (a) => (b) => b - a;
7+
/**
8+
* Compose (flow right): 5 + 10 - 2 = 13
9+
*/
10+
expect(compose(curriedSubtract(2), curriedAdd(10))(5)).toBe(13);
11+
});
12+
});

src/utils/transpile/compose.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Creates a new composite function that invokes the functions from right to left
3+
* @param {...function} functions: all the functions to invoke
4+
* @returns a composite function
5+
*/
6+
export default function compose(...functions) {
7+
return functions.reduce(
8+
(acc, currentFn) =>
9+
(...args) =>
10+
acc(currentFn(...args))
11+
);
12+
}

src/utils/transpile/index.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,43 @@ import React from "react";
22
import transform from "./transform";
33
import errorBoundary from "./errorBoundary";
44
import evalCode from "./evalCode";
5+
import compose from "./compose";
56

6-
export const generateElement = ({ code = "", scope = {} }, errorCallback) => {
7-
// NOTE: Remove trailing semicolon to get an actual expression.
8-
const codeTrimmed = code.trim().replace(/;$/, "");
7+
const jsxConst = 'const _jsxFileName = "";';
8+
const trimCode = (code) => code.trim().replace(/;$/, "");
9+
const spliceJsxConst = (code) => code.replace(jsxConst, "").trim();
10+
const addJsxConst = (code) => jsxConst + code;
11+
const wrapReturn = (code) => `return (${code})`;
12+
13+
export const generateElement = (
14+
{ code = "", scope = {}, enableTypeScript = true },
15+
errorCallback
16+
) => {
17+
/**
18+
* To enable TypeScript we need to transform the TS to JS code first,
19+
* splice off the JSX const, wrap the eval in a return statement, then
20+
* transform any imports. The two-phase approach is required to do
21+
* the implicit evaluation and not wrap leading Interface or Type
22+
* statements in the return.
23+
*/
24+
const transformed = compose(
25+
addJsxConst,
26+
transform({ transforms: ["imports"] }),
27+
wrapReturn,
28+
spliceJsxConst,
29+
trimCode,
30+
transform({ transforms: ["jsx", enableTypeScript && "typescript"] }),
31+
trimCode
32+
)(code);
933

10-
// NOTE: Workaround for classes and arrow functions.
11-
const transformed = transform(`return (${codeTrimmed})`).trim();
1234
return errorBoundary(
1335
evalCode(transformed, { React, ...scope }),
1436
errorCallback
1537
);
1638
};
1739

1840
export const renderElementAsync = (
19-
{ code = "", scope = {} },
41+
{ code = "", scope = {}, enableTypeScript = true },
2042
resultCallback,
2143
errorCallback
2244
// eslint-disable-next-line consistent-return
@@ -35,5 +57,10 @@ export const renderElementAsync = (
3557
);
3658
}
3759

38-
evalCode(transform(code), { React, ...scope, render });
60+
evalCode(
61+
transform({
62+
transforms: ["jsx", enableTypeScript && "typescript", "imports"],
63+
})(code),
64+
{ React, ...scope, render }
65+
);
3966
};

src/utils/transpile/transform.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { transform as _transform } from "sucrase";
22

3-
const opts = { transforms: ["jsx", "imports"] };
3+
const defaultTransforms = ["jsx", "imports"];
44

5-
export default (code) => _transform(code, opts).code;
5+
export default function transform(opts = {}) {
6+
const transforms = Array.isArray(opts.transforms)
7+
? opts.transforms.filter(Boolean)
8+
: defaultTransforms;
9+
10+
return (code) => _transform(code, { transforms }).code;
11+
}

src/utils/transpile/tsTransform.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { transform as _transform } from "sucrase";
2+
3+
export default (code) => {
4+
return _transform(code, { transforms: ["jsx", "typescript"] }).code;
5+
};

0 commit comments

Comments
 (0)