Skip to content

Commit 473b0c4

Browse files
authored
Merge pull request #3035 from plotly/feat/tsx-prop-types
add prop types to tsx components
2 parents 5fe20c7 + daccac8 commit 473b0c4

File tree

5 files changed

+171
-3
lines changed

5 files changed

+171
-3
lines changed

@plotly/dash-generator-test-component-typescript/base/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
dict(
1717
relative_package_path='dash_generator_test_component_typescript.js',
1818
namespace='dash_generator_test_component_typescript'
19-
)
19+
),
20+
{
21+
"dev_package_path": "proptypes.js",
22+
"dev_only": True,
23+
"namespace": 'dash_generator_test_component_typescript'
24+
}
2025
]
2126

2227
for _component in __all__:
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# tsx components don't have the `.propTypes` property set
2+
# Generate it instead with the provided metadata.json
3+
# for them to be able to report invalid prop
4+
5+
import os
6+
import re
7+
8+
9+
init_check_re = re.compile("proptypes.js")
10+
11+
missing_init_msg = """
12+
{warning_box}
13+
{title}
14+
{warning_box}
15+
16+
Add the following to `{namespace}/__init__.py` to enable
17+
runtime prop types validation with tsx components:
18+
19+
_js_dist.append(dict(
20+
dev_package_path="proptypes.js",
21+
namespace="{namespace}"
22+
))
23+
24+
"""
25+
26+
prop_type_file_template = """// AUTOGENERATED FILE - DO NOT EDIT
27+
28+
var PropTypes = window.PropTypes;
29+
30+
31+
{components_prop_types}
32+
"""
33+
34+
component_prop_types_template = (
35+
"window['{package_name}'].{component_name}.propTypes = {prop_types}"
36+
)
37+
38+
39+
def generate_type(type_name):
40+
def wrap(*_):
41+
return f"PropTypes.{type_name}"
42+
43+
return wrap
44+
45+
46+
def generate_union(prop_info):
47+
types = [generate_prop_type(t) for t in prop_info["value"]]
48+
return f"PropTypes.oneOfType([{','.join(types)}])"
49+
50+
51+
def generate_shape(prop_info):
52+
props = []
53+
for key, value in prop_info["value"].items():
54+
props.append(f"{key}:{generate_prop_type(value)}")
55+
inner = "{" + ",".join(props) + "}"
56+
return f"PropTypes.shape({inner})"
57+
58+
59+
def generate_array_of(prop_info):
60+
inner_type = generate_prop_type(prop_info["value"])
61+
return f"PropTypes.arrayOf({inner_type})"
62+
63+
64+
def generate_any(*_):
65+
return "PropTypes.any"
66+
67+
68+
def generate_enum(prop_info):
69+
values = str([v["value"] for v in prop_info["value"]])
70+
return f"PropTypes.oneOf({values})"
71+
72+
73+
def generate_object_of(prop_info):
74+
return f"PropTypes.objectOf({generate_prop_type(prop_info['value'])})"
75+
76+
77+
def generate_tuple(*_):
78+
# PropTypes don't have a tuple... just generate an array.
79+
return "PropTypes.array"
80+
81+
82+
prop_types = {
83+
"array": generate_type("array"),
84+
"arrayOf": generate_array_of,
85+
"object": generate_type("object"),
86+
"shape": generate_shape,
87+
"exact": generate_shape,
88+
"string": generate_type("string"),
89+
"bool": generate_type("bool"),
90+
"number": generate_type("number"),
91+
"node": generate_type("node"),
92+
"func": generate_any,
93+
"element": generate_type("element"),
94+
"union": generate_union,
95+
"any": generate_any,
96+
"custom": generate_any,
97+
"enum": generate_enum,
98+
"objectOf": generate_object_of,
99+
"tuple": generate_tuple,
100+
}
101+
102+
103+
def generate_prop_type(prop_info):
104+
return prop_types[prop_info["name"]](prop_info)
105+
106+
107+
def check_init(namespace):
108+
path = os.path.join(namespace, "__init__.py")
109+
if os.path.exists(path):
110+
with open(path, encoding="utf-8", mode="r") as f:
111+
if not init_check_re.search(f.read()):
112+
title = f"! Missing proptypes.js in `{namespace}/__init__.py` !"
113+
print(
114+
missing_init_msg.format(
115+
namespace=namespace,
116+
warning_box="!" * len(title),
117+
title=title,
118+
)
119+
)
120+
121+
122+
def generate_prop_types(
123+
metadata,
124+
package_name,
125+
):
126+
patched = []
127+
128+
for component_path, data in metadata.items():
129+
filename = component_path.split("/")[-1]
130+
extension = filename.split("/")[-1].split(".")[-1]
131+
if extension != "tsx":
132+
continue
133+
134+
component_name = filename.split(".")[0]
135+
136+
props = []
137+
for prop_name, prop_data in data.get("props", {}).items():
138+
props.append(f" {prop_name}:{generate_prop_type(prop_data['type'])}")
139+
140+
patched.append(
141+
component_prop_types_template.format(
142+
package_name=package_name,
143+
component_name=component_name,
144+
prop_types="{" + ",\n".join(props) + "}",
145+
)
146+
)
147+
148+
if patched:
149+
with open(
150+
os.path.join(package_name, "proptypes.js"), encoding="utf-8", mode="w"
151+
) as f:
152+
f.write(
153+
prop_type_file_template.format(
154+
components_prop_types="\n\n".join(patched)
155+
)
156+
)
157+
158+
check_init(package_name)

dash/development/_r_components_generation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ def generate_js_metadata(pkg_data, project_shortname):
278278
if len(alldist) > 1:
279279
for dep in range(len(alldist)):
280280
curr_dep = alldist[dep]
281-
rpp = curr_dep["relative_package_path"]
281+
rpp = curr_dep.get("relative_package_path", "")
282+
if not rpp:
283+
continue
282284

283285
async_or_dynamic = get_async_type(curr_dep)
284286

dash/development/component_generator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ._py_components_generation import generate_classes_files
1919
from ._jl_components_generation import generate_struct_file
2020
from ._jl_components_generation import generate_module
21+
from ._generate_prop_types import generate_prop_types
2122

2223
reserved_words = [
2324
"UNDEFINED",
@@ -135,6 +136,8 @@ def generate_components(
135136

136137
components = generate_classes_files(project_shortname, metadata, *generator_methods)
137138

139+
generate_prop_types(metadata, project_shortname)
140+
138141
with open(
139142
os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"
140143
) as f:

dash/resources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _filter_resources(
8282
s.get("external_only") or not self.config.serve_locally
8383
):
8484
filtered_resource["external_url"] = s["external_url"]
85-
elif "dev_package_path" in s and dev_bundles:
85+
elif "dev_package_path" in s and (dev_bundles or s.get("dev_only")):
8686
filtered_resource["relative_package_path"] = s["dev_package_path"]
8787
elif "relative_package_path" in s:
8888
filtered_resource["relative_package_path"] = s["relative_package_path"]

0 commit comments

Comments
 (0)