Skip to content

Commit

Permalink
Merge pull request #1956 from plotly/typescript-component-generator
Browse files Browse the repository at this point in the history
Add TypeScript components generation support.
  • Loading branch information
alexcjohnson authored Apr 27, 2022
2 parents 0fffb14 + c8a6f00 commit 3860484
Show file tree
Hide file tree
Showing 22 changed files with 23,438 additions and 82 deletions.
1 change: 1 addition & 0 deletions @plotly/dash-generator-test-component-typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Dash generator test component typescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const presets = [
'@babel/preset-env',
'@babel/preset-react'
];

const plugins = [];

module.exports = { presets, plugins };
23 changes: 23 additions & 0 deletions @plotly/dash-generator-test-component-typescript/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import json
import os as _os

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, 'package.json'))
with open(_filepath) as f:
package = json.load(f)

package_name = package['name'].replace(' ', '_').replace('-', '_')
__version__ = package['version']

from ._imports_ import * # noqa: F401, F403
from ._imports_ import __all__ # noqa: E402

_js_dist = [
dict(
relative_package_path='dash_generator_test_component_typescript.js',
namespace='dash_generator_test_component_typescript'
)
]

for _component in __all__:
setattr(locals()[_component], '_js_dist', _js_dist)
248 changes: 248 additions & 0 deletions @plotly/dash-generator-test-component-typescript/generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import child_process from 'child_process';
import path from 'path';
import R from 'ramda';

function getMetadata() {
return new Promise((resolve, reject) => {
const cp = child_process.spawn(
process.execPath,
[
path.resolve(__dirname, '..', '..', 'dash', 'extract-meta.js'),
'""', // ignore pattern
'""', // reserved keywords
path.join(__dirname, 'src', 'components')
],
// To debug `meta-ts.js` using pycharm debugger:
// comment `env` and add `MODULES_PATH=./node_modules`
// in the run config environment variables.
{
env: {MODULES_PATH: path.resolve(__dirname, './node_modules')},
cwd: __dirname
}
);
const meta = [];
const err = [];
cp.stdout.on('data', data => {
meta.push(data);
});
cp.stderr.on('data', data => {
err.push(data);
});
cp.on('close', code => {
if (code === 0) {
resolve(
R.values(JSON.parse(meta.join(''))).reduce((acc, c) => {
// Map them back to component name for easier access.
acc[c.displayName] = c;
return acc;
}, {})
);
} else {
reject(err.join(''));
}
});
cp.on('error', error => {
console.error(error);
reject(error);
});
});
}

describe('Test Typescript component metadata generation', () => {
let metadata;

beforeAll(async () => {
metadata = await getMetadata();
});

const propPath = (componentName, propName) => [
componentName,
'props',
propName
];

describe.each([
'TypeScriptComponent',
'TypeScriptClassComponent',
'MemoTypeScriptComponent'
])('Test prop type names', componentName => {
const getPropTypeName = (name, data) =>
R.path(propPath(componentName, name).concat('type', 'name'), data);
const testTypeFactory = (name, expectedType) => () =>
expect(getPropTypeName(name, metadata)).toBe(expectedType);

test(
`${componentName} string type`,
testTypeFactory('a_string', 'string')
);
test(
`${componentName} number type`,
testTypeFactory('a_number', 'number')
);
test(
`${componentName} array type`,
testTypeFactory('array_string', 'arrayOf')
);
test(`${componentName} object type`, testTypeFactory('obj', 'shape'));
test(`${componentName} union type`, testTypeFactory('union', 'union'));
test(
`${componentName} enum type`,
testTypeFactory('enum_string', 'enum')
);
test(
`${componentName} children React.Node`,
testTypeFactory('children', 'node')
);
test(
`${componentName} element JSX.Element`,
testTypeFactory('element', 'node')
);
test(
`${componentName} boolean type`,
testTypeFactory('a_bool', 'bool')
);
test(
`${componentName} setProps func`,
testTypeFactory('setProps', 'func')
);
});

describe('Test prop attributes', () => {
test('Required props', () => {
expect(
R.path(
propPath('TypeScriptComponent', 'required_string').concat(
'required'
),
metadata
)
).toBeTruthy();
expect(
R.path(
propPath('TypeScriptComponent', 'a_string').concat(
'required'
),
metadata
)
).toBeFalsy();
});
test('Component prop has comment', () => {
// Comments with `@` in them will not work due the way the typescript compiler handle them with jsdoc.
// To fix & add test once they add back the ability to get raw comments.
expect(
R.path(
propPath('TypeScriptComponent', 'required_string').concat(
'description'
),
metadata
)
).toBe('A string');
});
test('Enum options', () => {
expect(
R.path(
propPath('TypeScriptComponent', 'enum_string').concat(
'type',
'value'
),
metadata
)
).toStrictEqual([
{value: "'one'", computed: false},
{value: "'two'", computed: false}
]);
});
test('Union of number and string', () => {
const propType = R.path(
propPath('TypeScriptComponent', 'union').concat('type'),
metadata
);
expect(propType.value.map(R.prop('name'))).toStrictEqual([
'string',
'number'
]);
});
test('Union of shape and string', () => {
const propType = R.path(
propPath('TypeScriptComponent', 'union_shape').concat(
'type',
'value'
),
metadata
);
const types = propType.map(R.prop('name'));
expect(types).toHaveLength(2);
expect(types).toContainEqual('shape');
expect(types).toContainEqual('string');
});
test('Array of union of shapes and string', () => {
const propType = R.path(
propPath('TypeScriptComponent', 'array_union_shape').concat(
'type'
),
metadata
);
expect(propType.value.name).toBe('union');
expect(propType.value.value.length).toBe(2);
expect(propType.value.value[0].name).toBe('string');
expect(propType.value.value[1].name).toBe('shape');
});
test('Obj properties', () => {
const propType = R.path(
propPath('TypeScriptComponent', 'obj').concat('type', 'value'),
metadata
);
expect(propType.value.name).toBe('any');
expect(propType.label.name).toBe('string');
});
test.each(['TypeScriptComponent', 'TypeScriptClassComponent'])(
'Default props',
(componentName: string) => {
const defaultValue = (field: string) =>
R.path(
propPath(componentName, field).concat(
'defaultValue',
'value'
),
metadata
);
expect(defaultValue('string_default')).toBe("'default'");
expect(defaultValue('number_default')).toBe('42');
expect(defaultValue('bool_default')).toBe(
componentName === 'TypeScriptComponent' ? 'true' : 'false'
);
expect(defaultValue('null_default')).toBe('null');
expect(eval(`(${defaultValue('obj_default')})`)).toStrictEqual({
a: 'a',
b: 3
});
}
);
});

describe('Test component comments', () => {
test('Component has docstring', () => {
expect(
R.path(['TypeScriptComponent', 'description'], metadata)
).toBe('Component docstring');
});
test.each(['TypeScriptClassComponent', 'MemoTypeScriptComponent'])(
'Component with `@` in docstring',
componentName => {
expect(R.path([componentName, 'description'], metadata)).toBe(
'Description\n' +
'Example:\n```\n' +
'@app.callback(...)\n' +
'def on_click(*args):\n' +
' return 1\n' +
'```'
);
}
);
});
describe('Test mixed generation', () => {
test('Standard js component is parsed', () => {
expect(R.path(['StandardComponent'], metadata)).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
coverageProvider: "v8",
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
}
};
Loading

0 comments on commit 3860484

Please sign in to comment.