Skip to content

Commit 98ba7b2

Browse files
committed
Merge branch 'async-transform' of https://github.com/srmagura/react-live into srmagura-async-transform
2 parents b05f76b + 355f636 commit 98ba7b2

File tree

4 files changed

+128
-23
lines changed

4 files changed

+128
-23
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@
9090
"react live"
9191
],
9292
"jest": {
93+
"testEnvironment": "jsdom",
94+
"resetMocks": true,
9395
"rootDir": "./src",
9496
"testURL": "http://localhost/"
9597
},

src/components/Live/LiveProvider.js

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,54 @@ function LiveProvider({
1919
element: undefined,
2020
});
2121

22-
function transpile(newCode) {
23-
// Transpilation arguments
24-
const input = {
25-
code: transformCode ? transformCode(newCode) : newCode,
26-
scope,
22+
function transpileAsync(newCode) {
23+
const errorCallback = (error) => {
24+
setState({ error: error.toString(), element: undefined });
2725
};
2826

29-
const errorCallback = (error) =>
30-
setState({ error: error.toString(), element: undefined });
27+
// - transformCode may be synchronous or asynchronous.
28+
// - transformCode may throw an exception or return a rejected promise, e.g.
29+
// if newCode is invalid and cannot be transformed.
30+
// - Not using async-await to since it requires targeting ES 2017 or
31+
// importing regenerator-runtime... in the next major version of
32+
// react-live, should target ES 2017+
33+
try {
34+
const transformResult = transformCode ? transformCode(newCode) : newCode;
3135

32-
const renderElement = (element) => setState({ error: undefined, element });
36+
return Promise.resolve(transformResult)
37+
.then((transformedCode) => {
38+
const renderElement = (element) =>
39+
setState({ error: undefined, element });
3340

34-
try {
35-
if (noInline) {
36-
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
37-
renderElementAsync(input, renderElement, errorCallback);
38-
} else {
39-
renderElement(generateElement(input, errorCallback));
40-
}
41-
} catch (error) {
42-
errorCallback(error);
41+
// Transpilation arguments
42+
const input = {
43+
code: transformedCode,
44+
scope,
45+
};
46+
47+
if (noInline) {
48+
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
49+
renderElementAsync(input, renderElement, errorCallback);
50+
} else {
51+
renderElement(generateElement(input, errorCallback));
52+
}
53+
})
54+
.catch(errorCallback);
55+
} catch (e) {
56+
errorCallback(e);
57+
return Promise.resolve();
4358
}
4459
}
4560

61+
const onError = (error) => setState({ error: error.toString() });
62+
4663
useEffect(() => {
47-
transpile(code);
64+
transpileAsync(code).catch(onError);
4865
}, [code, scope, noInline, transformCode]);
4966

50-
const onChange = (newCode) => transpile(newCode);
51-
52-
const onError = (error) => setState({ error: error.toString() });
67+
const onChange = (newCode) => {
68+
transpileAsync(newCode).catch(onError);
69+
};
5370

5471
return (
5572
<LiveContext.Provider
@@ -76,7 +93,7 @@ LiveProvider.propTypes = {
7693
noInline: PropTypes.bool,
7794
scope: PropTypes.object,
7895
theme: PropTypes.object,
79-
transformCode: PropTypes.node,
96+
transformCode: PropTypes.func,
8097
};
8198

8299
LiveProvider.defaultProps = {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from 'react';
2+
import { act } from 'react-dom/test-utils';
3+
import { renderElementAsync } from '../../utils/transpile';
4+
import { mount } from 'enzyme';
5+
import LiveProvider from './LiveProvider';
6+
import { useContext } from 'react';
7+
import LiveContext from './LiveContext';
8+
9+
jest.mock('../../utils/transpile');
10+
11+
function waitAsync() {
12+
return act(() => new Promise(resolve => setTimeout(resolve, 0)));
13+
}
14+
15+
it('applies a synchronous transformCode function', () => {
16+
function transformCode(code) {
17+
return `render(<div>${code}</div>)`;
18+
}
19+
20+
mount(<LiveProvider code="hello" noInline transformCode={transformCode} />);
21+
22+
return waitAsync().then(() => {
23+
expect(renderElementAsync).toHaveBeenCalledTimes(1);
24+
expect(renderElementAsync.mock.calls[0][0].code).toBe(
25+
'render(<div>hello</div>)'
26+
);
27+
});
28+
});
29+
30+
it('applies an asynchronous transformCode function', () => {
31+
function transformCode(code) {
32+
return Promise.resolve(`render(<div>${code}</div>)`);
33+
}
34+
35+
mount(<LiveProvider code="hello" noInline transformCode={transformCode} />);
36+
37+
return waitAsync().then(() => {
38+
expect(renderElementAsync).toHaveBeenCalledTimes(1);
39+
expect(renderElementAsync.mock.calls[0][0].code).toBe(
40+
'render(<div>hello</div>)'
41+
);
42+
});
43+
});
44+
45+
function ErrorRenderer() {
46+
const { error } = useContext(LiveContext);
47+
return <div data-testid="handledError">{error?.message}</div>;
48+
}
49+
50+
it('catches errors from a synchronous transformCode function', () => {
51+
function transformCode() {
52+
throw new Error('testError');
53+
}
54+
55+
const wrapper = mount(
56+
<LiveProvider code="hello" noInline transformCode={transformCode}>
57+
<ErrorRenderer />
58+
</LiveProvider>
59+
);
60+
61+
return waitAsync().then(() => {
62+
expect(renderElementAsync).not.toHaveBeenCalled();
63+
64+
const handledErrorWrapper = wrapper.find('[data-testid="handledError"]');
65+
expect(handledErrorWrapper.text()).toBe('testError');
66+
});
67+
});
68+
69+
it('catches errors from an asynchronous transformCode function', () => {
70+
function transformCode() {
71+
return Promise.reject(new Error('testError'));
72+
}
73+
74+
const wrapper = mount(
75+
<LiveProvider code="hello" noInline transformCode={transformCode}>
76+
<ErrorRenderer />
77+
</LiveProvider>
78+
);
79+
80+
return waitAsync().then(() => {
81+
expect(renderElementAsync).not.toHaveBeenCalled();
82+
83+
const handledErrorWrapper = wrapper.find('[data-testid="handledError"]');
84+
expect(handledErrorWrapper.text()).toBe('testError');
85+
});
86+
});

typings/react-live.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type LiveProviderProps = Omit<DivProps, 'scope'> & {
1313
scope?: { [key: string]: any };
1414
code?: string;
1515
noInline?: boolean;
16-
transformCode?: (code: string) => string;
16+
transformCode?: (code: string) => (string | Promise<string>);
1717
language?: Language;
1818
disabled?: boolean;
1919
theme?: PrismTheme;

0 commit comments

Comments
 (0)