Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add prop types to tsx components #3035

Merged
merged 8 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
dict(
relative_package_path='dash_generator_test_component_typescript.js',
namespace='dash_generator_test_component_typescript'
)
),
{
"dev_package_path": "proptypes.js",
"dev_only": True,
"namespace": 'dash_generator_test_component_typescript'
}
]

for _component in __all__:
Expand Down
158 changes: 158 additions & 0 deletions dash/development/_generate_prop_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# tsx components don't have the `.propTypes` property set
# Generate it instead with the provided metadata.json
# for them to be able to report invalid prop

import os
import re


init_check_re = re.compile("proptypes.js")

missing_init_msg = """
{warning_box}
{title}
{warning_box}

Add the following to `{namespace}/__init__.py` to enable
runtime prop types validation with tsx components:

_js_dist.append(dict(
dev_package_path="proptypes.js",
namespace="{namespace}"
))

"""

prop_type_file_template = """// AUTOGENERATED FILE - DO NOT EDIT

var PropTypes = window.PropTypes;


{components_prop_types}
"""

component_prop_types_template = (
"window['{package_name}'].{component_name}.propTypes = {prop_types}"
)


def generate_type(type_name):
def wrap(*_):
return f"PropTypes.{type_name}"

return wrap


def generate_union(prop_info):
types = [generate_prop_type(t) for t in prop_info["value"]]
return f"PropTypes.oneOfType([{','.join(types)}])"


def generate_shape(prop_info):
props = []
for key, value in prop_info["value"].items():
props.append(f"{key}:{generate_prop_type(value)}")
inner = "{" + ",".join(props) + "}"
return f"PropTypes.shape({inner})"


def generate_array_of(prop_info):
inner_type = generate_prop_type(prop_info["value"])
return f"PropTypes.arrayOf({inner_type})"


def generate_any(*_):
return "PropTypes.any"


def generate_enum(prop_info):
values = str([v["value"] for v in prop_info["value"]])
return f"PropTypes.oneOf({values})"


def generate_object_of(prop_info):
return f"PropTypes.objectOf({generate_prop_type(prop_info['value'])})"


def generate_tuple(*_):
# PropTypes don't have a tuple... just generate an array.
return "PropTypes.array"


prop_types = {
"array": generate_type("array"),
"arrayOf": generate_array_of,
"object": generate_type("object"),
"shape": generate_shape,
"exact": generate_shape,
"string": generate_type("string"),
"bool": generate_type("bool"),
"number": generate_type("number"),
"node": generate_type("node"),
"func": generate_any,
"element": generate_type("element"),
"union": generate_union,
"any": generate_any,
"custom": generate_any,
"enum": generate_enum,
"objectOf": generate_object_of,
"tuple": generate_tuple,
}


def generate_prop_type(prop_info):
return prop_types[prop_info["name"]](prop_info)


def check_init(namespace):
path = os.path.join(namespace, "__init__.py")
if os.path.exists(path):
with open(path, encoding="utf-8", mode="r") as f:
if not init_check_re.search(f.read()):
title = f"! Missing proptypes.js in `{namespace}/__init__.py` !"
print(
missing_init_msg.format(
namespace=namespace,
warning_box="!" * len(title),
title=title,
)
)


def generate_prop_types(
metadata,
package_name,
):
patched = []

for component_path, data in metadata.items():
filename = component_path.split("/")[-1]
extension = filename.split("/")[-1].split(".")[-1]
if extension != "tsx":
continue

component_name = filename.split(".")[0]

props = []
for prop_name, prop_data in data.get("props", {}).items():
props.append(f" {prop_name}:{generate_prop_type(prop_data['type'])}")

patched.append(
component_prop_types_template.format(
package_name=package_name,
component_name=component_name,
prop_types="{" + ",\n".join(props) + "}",
)
)

if patched:
with open(
os.path.join(package_name, "proptypes.js"), encoding="utf-8", mode="w"
) as f:
f.write(
prop_type_file_template.format(
components_prop_types="\n\n".join(patched)
)
)

check_init(package_name)
4 changes: 3 additions & 1 deletion dash/development/_r_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ def generate_js_metadata(pkg_data, project_shortname):
if len(alldist) > 1:
for dep in range(len(alldist)):
curr_dep = alldist[dep]
rpp = curr_dep["relative_package_path"]
rpp = curr_dep.get("relative_package_path", "")
if not rpp:
continue

async_or_dynamic = get_async_type(curr_dep)

Expand Down
3 changes: 3 additions & 0 deletions dash/development/component_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ._py_components_generation import generate_classes_files
from ._jl_components_generation import generate_struct_file
from ._jl_components_generation import generate_module
from ._generate_prop_types import generate_prop_types

reserved_words = [
"UNDEFINED",
Expand Down Expand Up @@ -135,6 +136,8 @@ def generate_components(

components = generate_classes_files(project_shortname, metadata, *generator_methods)

generate_prop_types(metadata, project_shortname)

with open(
os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"
) as f:
Expand Down
2 changes: 1 addition & 1 deletion dash/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _filter_resources(
s.get("external_only") or not self.config.serve_locally
):
filtered_resource["external_url"] = s["external_url"]
elif "dev_package_path" in s and dev_bundles:
elif "dev_package_path" in s and (dev_bundles or s.get("dev_only")):
filtered_resource["relative_package_path"] = s["dev_package_path"]
elif "relative_package_path" in s:
filtered_resource["relative_package_path"] = s["relative_package_path"]
Expand Down