Skip to content

Commit 355f636

Browse files
committed
Support async transformCode, catch transformCode errors
1 parent 68fce44 commit 355f636

File tree

4 files changed

+129
-24
lines changed

4 files changed

+129
-24
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
"react live"
9494
],
9595
"jest": {
96+
"testEnvironment": "jsdom",
97+
"resetMocks": true,
9698
"rootDir": "./src",
9799
"setupFiles": [
98100
"../jest.setup.js"

src/components/Live/LiveProvider.js

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,55 @@ function LiveProvider({
2020
element: undefined
2121
});
2222

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

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

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

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

63+
const onError = error => setState({ error: error.toString() });
64+
4865
useEffect(() => {
49-
transpile(code);
66+
transpileAsync(code).catch(onError);
5067
}, [code, scope, noInline, transformCode, transpileOptions]);
5168

52-
const onChange = newCode => transpile(newCode);
53-
54-
const onError = error => setState({ error: error.toString() });
69+
const onChange = newCode => {
70+
transpileAsync(newCode).catch(onError);
71+
};
5572

5673
return (
5774
<LiveContext.Provider
@@ -78,7 +95,7 @@ LiveProvider.propTypes = {
7895
noInline: PropTypes.bool,
7996
scope: PropTypes.object,
8097
theme: PropTypes.object,
81-
transformCode: PropTypes.node,
98+
transformCode: PropTypes.func,
8299
transpileOptions: PropTypes.object
83100
};
84101

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
@@ -16,7 +16,7 @@ export type LiveProviderProps = Omit<DivProps, 'scope'> & {
1616
scope?: { [key: string]: any };
1717
code?: string;
1818
noInline?: boolean;
19-
transformCode?: (code: string) => string;
19+
transformCode?: (code: string) => (string | Promise<string>);
2020
transpileOptions?: TranspileOptions;
2121
language?: Language;
2222
disabled?: boolean;

0 commit comments

Comments
 (0)