Skip to content

Commit 3860484

Browse files
authored
Merge pull request #1956 from plotly/typescript-component-generator
Add TypeScript components generation support.
2 parents 0fffb14 + c8a6f00 commit 3860484

22 files changed

+23438
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Dash generator test component typescript
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const presets = [
2+
'@babel/preset-env',
3+
'@babel/preset-react'
4+
];
5+
6+
const plugins = [];
7+
8+
module.exports = { presets, plugins };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import json
2+
import os as _os
3+
4+
_basepath = _os.path.dirname(__file__)
5+
_filepath = _os.path.abspath(_os.path.join(_basepath, 'package.json'))
6+
with open(_filepath) as f:
7+
package = json.load(f)
8+
9+
package_name = package['name'].replace(' ', '_').replace('-', '_')
10+
__version__ = package['version']
11+
12+
from ._imports_ import * # noqa: F401, F403
13+
from ._imports_ import __all__ # noqa: E402
14+
15+
_js_dist = [
16+
dict(
17+
relative_package_path='dash_generator_test_component_typescript.js',
18+
namespace='dash_generator_test_component_typescript'
19+
)
20+
]
21+
22+
for _component in __all__:
23+
setattr(locals()[_component], '_js_dist', _js_dist)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import child_process from 'child_process';
2+
import path from 'path';
3+
import R from 'ramda';
4+
5+
function getMetadata() {
6+
return new Promise((resolve, reject) => {
7+
const cp = child_process.spawn(
8+
process.execPath,
9+
[
10+
path.resolve(__dirname, '..', '..', 'dash', 'extract-meta.js'),
11+
'""', // ignore pattern
12+
'""', // reserved keywords
13+
path.join(__dirname, 'src', 'components')
14+
],
15+
// To debug `meta-ts.js` using pycharm debugger:
16+
// comment `env` and add `MODULES_PATH=./node_modules`
17+
// in the run config environment variables.
18+
{
19+
env: {MODULES_PATH: path.resolve(__dirname, './node_modules')},
20+
cwd: __dirname
21+
}
22+
);
23+
const meta = [];
24+
const err = [];
25+
cp.stdout.on('data', data => {
26+
meta.push(data);
27+
});
28+
cp.stderr.on('data', data => {
29+
err.push(data);
30+
});
31+
cp.on('close', code => {
32+
if (code === 0) {
33+
resolve(
34+
R.values(JSON.parse(meta.join(''))).reduce((acc, c) => {
35+
// Map them back to component name for easier access.
36+
acc[c.displayName] = c;
37+
return acc;
38+
}, {})
39+
);
40+
} else {
41+
reject(err.join(''));
42+
}
43+
});
44+
cp.on('error', error => {
45+
console.error(error);
46+
reject(error);
47+
});
48+
});
49+
}
50+
51+
describe('Test Typescript component metadata generation', () => {
52+
let metadata;
53+
54+
beforeAll(async () => {
55+
metadata = await getMetadata();
56+
});
57+
58+
const propPath = (componentName, propName) => [
59+
componentName,
60+
'props',
61+
propName
62+
];
63+
64+
describe.each([
65+
'TypeScriptComponent',
66+
'TypeScriptClassComponent',
67+
'MemoTypeScriptComponent'
68+
])('Test prop type names', componentName => {
69+
const getPropTypeName = (name, data) =>
70+
R.path(propPath(componentName, name).concat('type', 'name'), data);
71+
const testTypeFactory = (name, expectedType) => () =>
72+
expect(getPropTypeName(name, metadata)).toBe(expectedType);
73+
74+
test(
75+
`${componentName} string type`,
76+
testTypeFactory('a_string', 'string')
77+
);
78+
test(
79+
`${componentName} number type`,
80+
testTypeFactory('a_number', 'number')
81+
);
82+
test(
83+
`${componentName} array type`,
84+
testTypeFactory('array_string', 'arrayOf')
85+
);
86+
test(`${componentName} object type`, testTypeFactory('obj', 'shape'));
87+
test(`${componentName} union type`, testTypeFactory('union', 'union'));
88+
test(
89+
`${componentName} enum type`,
90+
testTypeFactory('enum_string', 'enum')
91+
);
92+
test(
93+
`${componentName} children React.Node`,
94+
testTypeFactory('children', 'node')
95+
);
96+
test(
97+
`${componentName} element JSX.Element`,
98+
testTypeFactory('element', 'node')
99+
);
100+
test(
101+
`${componentName} boolean type`,
102+
testTypeFactory('a_bool', 'bool')
103+
);
104+
test(
105+
`${componentName} setProps func`,
106+
testTypeFactory('setProps', 'func')
107+
);
108+
});
109+
110+
describe('Test prop attributes', () => {
111+
test('Required props', () => {
112+
expect(
113+
R.path(
114+
propPath('TypeScriptComponent', 'required_string').concat(
115+
'required'
116+
),
117+
metadata
118+
)
119+
).toBeTruthy();
120+
expect(
121+
R.path(
122+
propPath('TypeScriptComponent', 'a_string').concat(
123+
'required'
124+
),
125+
metadata
126+
)
127+
).toBeFalsy();
128+
});
129+
test('Component prop has comment', () => {
130+
// Comments with `@` in them will not work due the way the typescript compiler handle them with jsdoc.
131+
// To fix & add test once they add back the ability to get raw comments.
132+
expect(
133+
R.path(
134+
propPath('TypeScriptComponent', 'required_string').concat(
135+
'description'
136+
),
137+
metadata
138+
)
139+
).toBe('A string');
140+
});
141+
test('Enum options', () => {
142+
expect(
143+
R.path(
144+
propPath('TypeScriptComponent', 'enum_string').concat(
145+
'type',
146+
'value'
147+
),
148+
metadata
149+
)
150+
).toStrictEqual([
151+
{value: "'one'", computed: false},
152+
{value: "'two'", computed: false}
153+
]);
154+
});
155+
test('Union of number and string', () => {
156+
const propType = R.path(
157+
propPath('TypeScriptComponent', 'union').concat('type'),
158+
metadata
159+
);
160+
expect(propType.value.map(R.prop('name'))).toStrictEqual([
161+
'string',
162+
'number'
163+
]);
164+
});
165+
test('Union of shape and string', () => {
166+
const propType = R.path(
167+
propPath('TypeScriptComponent', 'union_shape').concat(
168+
'type',
169+
'value'
170+
),
171+
metadata
172+
);
173+
const types = propType.map(R.prop('name'));
174+
expect(types).toHaveLength(2);
175+
expect(types).toContainEqual('shape');
176+
expect(types).toContainEqual('string');
177+
});
178+
test('Array of union of shapes and string', () => {
179+
const propType = R.path(
180+
propPath('TypeScriptComponent', 'array_union_shape').concat(
181+
'type'
182+
),
183+
metadata
184+
);
185+
expect(propType.value.name).toBe('union');
186+
expect(propType.value.value.length).toBe(2);
187+
expect(propType.value.value[0].name).toBe('string');
188+
expect(propType.value.value[1].name).toBe('shape');
189+
});
190+
test('Obj properties', () => {
191+
const propType = R.path(
192+
propPath('TypeScriptComponent', 'obj').concat('type', 'value'),
193+
metadata
194+
);
195+
expect(propType.value.name).toBe('any');
196+
expect(propType.label.name).toBe('string');
197+
});
198+
test.each(['TypeScriptComponent', 'TypeScriptClassComponent'])(
199+
'Default props',
200+
(componentName: string) => {
201+
const defaultValue = (field: string) =>
202+
R.path(
203+
propPath(componentName, field).concat(
204+
'defaultValue',
205+
'value'
206+
),
207+
metadata
208+
);
209+
expect(defaultValue('string_default')).toBe("'default'");
210+
expect(defaultValue('number_default')).toBe('42');
211+
expect(defaultValue('bool_default')).toBe(
212+
componentName === 'TypeScriptComponent' ? 'true' : 'false'
213+
);
214+
expect(defaultValue('null_default')).toBe('null');
215+
expect(eval(`(${defaultValue('obj_default')})`)).toStrictEqual({
216+
a: 'a',
217+
b: 3
218+
});
219+
}
220+
);
221+
});
222+
223+
describe('Test component comments', () => {
224+
test('Component has docstring', () => {
225+
expect(
226+
R.path(['TypeScriptComponent', 'description'], metadata)
227+
).toBe('Component docstring');
228+
});
229+
test.each(['TypeScriptClassComponent', 'MemoTypeScriptComponent'])(
230+
'Component with `@` in docstring',
231+
componentName => {
232+
expect(R.path([componentName, 'description'], metadata)).toBe(
233+
'Description\n' +
234+
'Example:\n```\n' +
235+
'@app.callback(...)\n' +
236+
'def on_click(*args):\n' +
237+
' return 1\n' +
238+
'```'
239+
);
240+
}
241+
);
242+
});
243+
describe('Test mixed generation', () => {
244+
test('Standard js component is parsed', () => {
245+
expect(R.path(['StandardComponent'], metadata)).toBeDefined();
246+
});
247+
});
248+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
coverageProvider: "v8",
3+
transform: {
4+
'^.+\\.(ts|tsx)?$': 'ts-jest',
5+
}
6+
};

0 commit comments

Comments
 (0)