diff --git a/@plotly/dash-generator-test-component-typescript/base/__init__.py b/@plotly/dash-generator-test-component-typescript/base/__init__.py index eb52e93d6d..b81643cce9 100644 --- a/@plotly/dash-generator-test-component-typescript/base/__init__.py +++ b/@plotly/dash-generator-test-component-typescript/base/__init__.py @@ -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__: diff --git a/dash/development/_generate_prop_types.py b/dash/development/_generate_prop_types.py new file mode 100644 index 0000000000..7990fdc0c0 --- /dev/null +++ b/dash/development/_generate_prop_types.py @@ -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) diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index c2b0bffd4b..9ad938744c 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -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) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 338ca3d144..352ce9f663 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -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", @@ -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: diff --git a/dash/resources.py b/dash/resources.py index 2813da951f..2f05e9c2c0 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -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"]