Repository: plotly/dash

Issue:
Modify 'transpiler' such that R components may import non-transpiled functions
Currently, the Python-based 'transpiler' invoked using `dash-generate-components` does not support generating R code which is either not based on entries in `metadata.json`, or expressly declared within the 'transpiler'.

A basic, modular system to seamlessly include both internal ("hidden") and exported (i.e. available in the package `NAMESPACE`) R functions would be beneficial to component library developers. 

Use cases have already been discussed, both for `dash-bio` and `dash-table`.

We should implement changes to the 'transpiler' that will enable
- [x] inspection of the R package directory for a pre-existing `utils.R` script, whose functions will not be exported via `NAMESPACE`
- [x] inspection of the R package directory for _any other_ R scripts, whose R functions will be collected and exposed via `export` statements in `NAMESPACE`
- [x] supplying a list of additional R package dependencies to `dash-generate-components`, which will be inserted into the package `DESCRIPTION` file

@alexcjohnson @chriddyp @nicolaskruchten @Marc-Andre-Rivet @shammamah 


Discussion:

Resolving patch:
diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py
--- a/dash/development/_r_components_generation.py
+++ b/dash/development/_r_components_generation.py
@@ -6,6 +6,7 @@
 import shutil
 import glob
 import importlib
+import re
 
 from ._all_keywords import r_keywords
 from ._py_components_generation import reorder_props
@@ -14,7 +15,7 @@
 # Declaring longer string templates as globals to improve
 # readability, make method logic clearer to anyone inspecting
 # code below
-r_component_string = '''{prefix}{name} <- function({default_argtext}{wildcards}) {{
+r_component_string = """{prefix}{name} <- function({default_argtext}{wildcards}) {{
     {wildcard_declaration}
     component <- list(
         props = list({default_paramtext}{wildcards}),
@@ -27,33 +28,37 @@
     component$props <- filter_null(component$props)
 
     structure(component, class = c('dash_component', 'list'))
-}}'''  # noqa:E501
+}}
+
+"""  # noqa:E501
 
 # the following strings represent all the elements in an object
 # of the html_dependency class, which will be propagated by
 # iterating over _js_dist in __init__.py
-frame_open_template = '''.{rpkgname}_js_metadata <- function() {{
-deps_metadata <- list('''
+frame_open_template = """.{rpkgname}_js_metadata <- function() {{
+deps_metadata <- list("""
 
-frame_element_template = '''`{dep_name}` = structure(list(name = "{dep_name}",
+frame_element_template = """`{dep_name}` = structure(list(name = "{dep_name}",
 version = "{project_ver}", src = list(href = NULL,
 file = "deps"), meta = NULL,
 script = "{dep_rpp}",
 stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}",
-all_files = FALSE), class = "html_dependency")'''
+all_files = FALSE), class = "html_dependency")"""
 
-frame_body_template = '''`{project_shortname}` = structure(list(name = "{project_shortname}",
+frame_body_template = """`{project_shortname}` = structure(list(name = "{project_shortname}",
 version = "{project_ver}", src = list(href = NULL,
 file = "deps"), meta = NULL,
 script = "{dep_rpp}",
 stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}",
-all_files = FALSE), class = "html_dependency")'''  # noqa:E501
+all_files = FALSE), class = "html_dependency")"""  # noqa:E501
 
-frame_close_template = ''')
+frame_close_template = """)
 return(deps_metadata)
-}'''
+}
+
+"""
 
-help_string = '''% Auto-generated: do not edit by hand
+help_string = """% Auto-generated: do not edit by hand
 \\name{{{prefix}{name}}}
 
 \\alias{{{prefix}{name}}}
@@ -71,16 +76,17 @@
 \\arguments{{
 {item_text}
 }}
-'''
 
-description_template = '''Package: {package_name}
+"""
+
+description_template = """Package: {package_name}
 Title: {package_description}
 Version: {package_version}
 Authors @R: as.person(c({package_author}))
 Description: {package_description}
-Depends: R (>= 3.0.2)
-Imports: dashR
-Suggests: testthat, roxygen2
+Depends: R (>= 3.0.2){package_depends}
+Imports: dashR{package_imports}
+Suggests: {package_suggests}
 License: {package_license}
 URL: {package_url}
 BugReports: {package_issues}
@@ -88,9 +94,9 @@
 LazyData: true
 Author: {package_author_no_email}
 Maintainer: {package_author}
-'''
+"""
 
-rbuild_ignore_string = r'''# ignore JS config files/folders
+rbuild_ignore_string = r"""# ignore JS config files/folders
 node_modules/
 coverage/
 src/
@@ -119,9 +125,9 @@
 LICENSE.txt
 ^.*\.Rproj$
 ^\.Rproj\.user$
-'''
+"""
 
-pkghelp_stub = '''% Auto-generated: do not edit by hand
+pkghelp_stub = """% Auto-generated: do not edit by hand
 \\docType{{package}}
 \\name{{{package_name}-package}}
 \\alias{{{package_name}}}
@@ -139,7 +145,8 @@
 \\author{{
 \\strong{{Maintainer}}: {package_author}
 }}
-'''
+
+"""
 
 
 # pylint: disable=R0914
@@ -152,72 +159,63 @@ def generate_class_string(name, props, project_shortname, prefix):
 
     prop_keys = list(props.keys())
 
-    wildcards = ''
-    wildcard_declaration = ''
-    wildcard_names = ''
+    wildcards = ""
+    wildcard_declaration = ""
+    wildcard_names = ""
 
-    if any('-*' in key for key in prop_keys):
-        wildcards = ', ...'
-        wildcard_declaration =\
-            '\n    wildcard_names = names(assert_valid_wildcards(...))\n'
-        wildcard_names = ', wildcard_names'
+    if any("-*" in key for key in prop_keys):
+        wildcards = ", ..."
+        wildcard_declaration = (
+            "\n    wildcard_names = names(assert_valid_wildcards(...))\n"
+        )
+        wildcard_names = ", wildcard_names"
 
-    default_paramtext = ''
-    default_argtext = ''
-    default_wildcards = ''
+    default_paramtext = ""
+    default_argtext = ""
+    default_wildcards = ""
 
     # Produce a string with all property names other than WCs
     prop_names = ", ".join(
-        '\'{}\''.format(p)
+        "'{}'".format(p)
         for p in prop_keys
-        if '*' not in p and
-        p not in ['setProps']
+        if "*" not in p and p not in ["setProps"]
     )
 
     # in R, we set parameters with no defaults to NULL
     # Here we'll do that if no default value exists
-    default_wildcards += ", ".join(
-        '\'{}\''.format(p)
-        for p in prop_keys
-        if '*' in p
-    )
+    default_wildcards += ", ".join("'{}'".format(p)
+                                   for p in prop_keys if "*" in p)
 
-    if default_wildcards == '':
-        default_wildcards = 'NULL'
+    if default_wildcards == "":
+        default_wildcards = "NULL"
     else:
-        default_wildcards = 'c({})'.format(default_wildcards)
+        default_wildcards = "c({})".format(default_wildcards)
 
     # Filter props to remove those we don't want to expose
     for item in prop_keys[:]:
-        if item.endswith('-*') \
-                or item in r_keywords \
-                or item == 'setProps':
+        if item.endswith("-*") or item in r_keywords or item == "setProps":
             prop_keys.remove(item)
 
-    default_argtext += ", ".join(
-        '{}=NULL'.format(p)
-        for p in prop_keys
-    )
+    default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys)
 
     # pylint: disable=C0301
     default_paramtext += ", ".join(
-        '{0}={0}'.format(p)
-        if p != "children" else
-        '{}=children'
-        .format(p)
+        "{0}={0}".format(p) if p != "children" else "{}=children".format(p)
         for p in prop_keys
     )
 
-    return r_component_string.format(prefix=prefix,
-                                     name=name,
-                                     default_argtext=default_argtext,
-                                     wildcards=wildcards,
-                                     wildcard_declaration=wildcard_declaration,
-                                     default_paramtext=default_paramtext,
-                                     project_shortname=project_shortname,
-                                     prop_names=prop_names,
-                                     wildcard_names=wildcard_names,
-                                     package_name=package_name)
+    return r_component_string.format(
+        prefix=prefix,
+        name=name,
+        default_argtext=default_argtext,
+        wildcards=wildcards,
+        wildcard_declaration=wildcard_declaration,
+        default_paramtext=default_paramtext,
+        project_shortname=project_shortname,
+        prop_names=prop_names,
+        wildcard_names=wildcard_names,
+        package_name=package_name,
+    )
 
 
 # pylint: disable=R0914
@@ -239,8 +237,8 @@ def generate_js_metadata(pkg_data, project_shortname):
     # import component library module into sys
     mod = sys.modules[project_shortname]
 
-    jsdist = getattr(mod, '_js_dist', [])
-    project_ver = pkg_data.get('version')
+    jsdist = getattr(mod, "_js_dist", [])
+    project_ver = pkg_data.get("version")
 
     rpkgname = snake_case_to_camel_case(project_shortname)
 
@@ -257,29 +255,32 @@ def generate_js_metadata(pkg_data, project_shortname):
     # pylint: disable=consider-using-enumerate
     if len(jsdist) > 1:
         for dep in range(len(jsdist)):
-            if 'dash_' in jsdist[dep]['relative_package_path']:
-                dep_name = jsdist[dep]['relative_package_path'].split('.')[0]
+            if "dash_" in jsdist[dep]["relative_package_path"]:
+                dep_name = jsdist[dep]["relative_package_path"].split(".")[0]
             else:
-                dep_name = '{}_{}'.format(project_shortname, str(dep))
+                dep_name = "{}_{}".format(project_shortname, str(dep))
                 project_ver = str(dep)
-            function_frame += [frame_element_template.format(
-                dep_name=dep_name,
-                project_ver=project_ver,
-                rpkgname=rpkgname,
-                project_shortname=project_shortname,
-                dep_rpp=jsdist[dep]['relative_package_path']
-            )]
-            function_frame_body = ',\n'.join(function_frame)
+            function_frame += [
+                frame_element_template.format(
+                    dep_name=dep_name,
+                    project_ver=project_ver,
+                    rpkgname=rpkgname,
+                    project_shortname=project_shortname,
+                    dep_rpp=jsdist[dep]["relative_package_path"],
+                )
+            ]
+            function_frame_body = ",\n".join(function_frame)
     elif len(jsdist) == 1:
-        function_frame_body = frame_body_template. \
-            format(project_shortname=project_shortname,
-                   project_ver=project_ver,
-                   rpkgname=rpkgname,
-                   dep_rpp=jsdist[0]['relative_package_path'])
+        function_frame_body = frame_body_template.format(
+            project_shortname=project_shortname,
+            project_ver=project_ver,
+            rpkgname=rpkgname,
+            dep_rpp=jsdist[0]["relative_package_path"],
+        )
 
-    function_string = ''.join([function_frame_open,
-                               function_frame_body,
-                               frame_close_template])
+    function_string = "".join(
+        [function_frame_open, function_frame_body, frame_close_template]
+    )
 
     return function_string
 
@@ -300,52 +301,45 @@ def write_help_file(name, props, description, prefix):
     writes an R help file to the man directory for the generated R package
 
     """
-    file_name = '{}{}.Rd'.format(prefix, name)
+    file_name = "{}{}.Rd".format(prefix, name)
 
-    default_argtext = ''
-    item_text = ''
+    default_argtext = ""
+    item_text = ""
 
     prop_keys = list(props.keys())
 
-    has_wildcards = any('-*' in key for key in prop_keys)
+    has_wildcards = any("-*" in key for key in prop_keys)
 
     # Filter props to remove those we don't want to expose
     for item in prop_keys[:]:
-        if item.endswith('-*') \
-                or item in r_keywords \
-                or item == 'setProps':
+        if item.endswith("-*") or item in r_keywords or item == "setProps":
             prop_keys.remove(item)
 
-    default_argtext += ", ".join(
-        '{}=NULL'.format(p)
-        for p in prop_keys
-    )
+    default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys)
 
     item_text += "\n\n".join(
-        '\\item{{{}}}{{{}}}'.format(p, props[p]['description'])
+        "\\item{{{}}}{{{}}}".format(p, props[p]["description"])
         for p in prop_keys
     )
 
     if has_wildcards:
-        item_text += '\n\n\\item{...}{wildcards: `data-*` or `aria-*`}'
-        default_argtext += ', ...'
-
-    file_path = os.path.join('man', file_name)
-    with open(file_path, 'w') as f:
-        f.write(help_string.format(
-            prefix=prefix,
-            name=name,
-            default_argtext=default_argtext,
-            item_text=item_text,
-            description=description.replace('\n', ' ')
-        ))
-
-
-def write_class_file(name,
-                     props,
-                     description,
-                     project_shortname,
-                     prefix=None):
+        item_text += "\n\n\\item{...}{wildcards: `data-*` or `aria-*`}"
+        default_argtext += ", ..."
+
+    file_path = os.path.join("man", file_name)
+    with open(file_path, "w") as f:
+        f.write(
+            help_string.format(
+                prefix=prefix,
+                name=name,
+                default_argtext=default_argtext,
+                item_text=item_text,
+                description=description.replace("\n", " "),
+            )
+        )
+
+
+def write_class_file(name, props, description, project_shortname, prefix=None):
     props = reorder_props(props=props)
 
     # generate the R help pages for each of the Dash components that we
@@ -353,29 +347,21 @@ def write_class_file(name,
     # we may eventually be able to generate similar documentation using
     # doxygen and an R plugin, but for now we'll just do it on our own
     # from within Python
-    write_help_file(
-        name,
-        props,
-        description,
-        prefix
-    )
+    write_help_file(name, props, description, prefix)
 
-    import_string =\
-        "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
-    class_string = generate_class_string(
-        name,
-        props,
-        project_shortname,
-        prefix
-    )
+    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
+    class_string = generate_class_string(name,
+                                         props,
+                                         project_shortname,
+                                         prefix)
     file_name = "{}{}.R".format(prefix, name)
 
-    file_path = os.path.join('R', file_name)
-    with open(file_path, 'w') as f:
+    file_path = os.path.join("R", file_name)
+    with open(file_path, "w") as f:
         f.write(import_string)
         f.write(class_string)
 
-    print('Generated {}'.format(file_name))
+    print("Generated {}".format(file_name))
 
 
 def write_js_metadata(pkg_data, project_shortname):
@@ -392,40 +378,44 @@ def write_js_metadata(pkg_data, project_shortname):
 
     """
     function_string = generate_js_metadata(
-        pkg_data=pkg_data,
-        project_shortname=project_shortname
+        pkg_data=pkg_data, project_shortname=project_shortname
     )
     file_name = "internal.R"
 
     # the R source directory for the package won't exist on first call
     # create the R directory if it is missing
-    if not os.path.exists('R'):
-        os.makedirs('R')
+    if not os.path.exists("R"):
+        os.makedirs("R")
 
-    file_path = os.path.join('R', file_name)
-    with open(file_path, 'w') as f:
+    file_path = os.path.join("R", file_name)
+    with open(file_path, "w") as f:
         f.write(function_string)
 
     # now copy over all JS dependencies from the (Python) components dir
     # the inst/lib directory for the package won't exist on first call
     # create this directory if it is missing
-    if not os.path.exists('inst/deps'):
-        os.makedirs('inst/deps')
+    if not os.path.exists("inst/deps"):
+        os.makedirs("inst/deps")
 
-    for javascript in glob.glob('{}/*.js'.format(project_shortname)):
-        shutil.copy(javascript, 'inst/deps/')
+    for javascript in glob.glob("{}/*.js".format(project_shortname)):
+        shutil.copy(javascript, "inst/deps/")
 
-    for css in glob.glob('{}/*.css'.format(project_shortname)):
-        shutil.copy(css, 'inst/deps/')
+    for css in glob.glob("{}/*.css".format(project_shortname)):
+        shutil.copy(css, "inst/deps/")
 
-    for sourcemap in glob.glob('{}/*.map'.format(project_shortname)):
-        shutil.copy(sourcemap, 'inst/deps/')
+    for sourcemap in glob.glob("{}/*.map".format(project_shortname)):
+        shutil.copy(sourcemap, "inst/deps/")
 
 
-# pylint: disable=R0914
-def generate_rpkg(pkg_data,
-                  project_shortname,
-                  export_string):
+# pylint: disable=R0914, R0913, R0912, R0915
+def generate_rpkg(
+        pkg_data,
+        project_shortname,
+        export_string,
+        package_depends,
+        package_imports,
+        package_suggests
+):
     """
     Generate documents for R package creation
 
@@ -444,59 +434,74 @@ def generate_rpkg(pkg_data,
     # does not exist in package.json
 
     package_name = snake_case_to_camel_case(project_shortname)
-    lib_name = pkg_data.get('name')
-    package_description = pkg_data.get('description', '')
-    package_version = pkg_data.get('version', '0.0.1')
+    lib_name = pkg_data.get("name")
+    package_description = pkg_data.get("description", "")
+    package_version = pkg_data.get("version", "0.0.1")
+
+    # remove leading and trailing commas
+    if package_depends:
+        package_depends = ", " + package_depends.strip(",").lstrip()
 
-    if 'bugs' in pkg_data.keys():
-        package_issues = pkg_data['bugs'].get('url', '')
+    if package_imports:
+        package_imports = ", " + package_imports.strip(",").lstrip()
+
+    if package_suggests:
+        package_suggests = package_suggests.strip(",").lstrip()
+
+    if "bugs" in pkg_data.keys():
+        package_issues = pkg_data["bugs"].get("url", "")
     else:
-        package_issues = ''
+        package_issues = ""
         print(
-            'Warning: a URL for bug reports was '
-            'not provided. Empty string inserted.',
-            file=sys.stderr
+            "Warning: a URL for bug reports was "
+            "not provided. Empty string inserted.",
+            file=sys.stderr,
         )
 
-    if 'homepage' in pkg_data.keys():
-        package_url = pkg_data.get('homepage', '')
+    if "homepage" in pkg_data.keys():
+        package_url = pkg_data.get("homepage", "")
     else:
-        package_url = ''
+        package_url = ""
         print(
-            'Warning: a homepage URL was not provided. Empty string inserted.',
-            file=sys.stderr
+            "Warning: a homepage URL was not provided. Empty string inserted.",
+            file=sys.stderr,
         )
 
-    package_author = pkg_data.get('author')
+    package_author = pkg_data.get("author")
 
-    package_author_no_email = package_author.split(" <")[0] + ' [aut]'
+    package_author_no_email = package_author.split(" <")[0] + " [aut]"
 
-    if not (os.path.isfile('LICENSE') or os.path.isfile('LICENSE.txt')):
-        package_license = pkg_data.get('license', '')
+    if not (os.path.isfile("LICENSE") or os.path.isfile("LICENSE.txt")):
+        package_license = pkg_data.get("license", "")
     else:
-        package_license = pkg_data.get('license', '') + ' + file LICENSE'
+        package_license = pkg_data.get("license", "") + " + file LICENSE"
         # R requires that the LICENSE.txt file be named LICENSE
-        if not os.path.isfile('LICENSE'):
+        if not os.path.isfile("LICENSE"):
             os.symlink("LICENSE.txt", "LICENSE")
 
-    import_string =\
-        '# AUTO GENERATED FILE - DO NOT EDIT\n\n'
+    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
+    packages_string = ''
+
+    rpackage_list = package_depends.split(', ') + package_imports.split(', ')
+    rpackage_list = filter(bool, rpackage_list)
+
+    if rpackage_list:
+        for rpackage in rpackage_list:
+            packages_string += "\nimport({})\n".format(rpackage)
 
-    pkghelp_stub_path = os.path.join('man', package_name + '-package.Rd')
+    pkghelp_stub_path = os.path.join("man", package_name + "-package.Rd")
 
     # generate the internal (not exported to the user) functions which
     # supply the JavaScript dependencies to the dashR package.
     # this avoids having to generate an RData file from within Python.
-    write_js_metadata(
-        pkg_data=pkg_data,
-        project_shortname=project_shortname
-    )
+    write_js_metadata(pkg_data=pkg_data, project_shortname=project_shortname)
 
-    with open('NAMESPACE', 'w') as f:
+    with open("NAMESPACE", "w") as f:
         f.write(import_string)
         f.write(export_string)
+        f.write(packages_string)
 
-    with open('.Rbuildignore', 'w') as f2:
+    with open(".Rbuildignore", "w") as f2:
         f2.write(rbuild_ignore_string)
 
     # Write package stub files for R online help, generate if
@@ -504,7 +509,7 @@ def generate_rpkg(pkg_data,
     # for R users to bring up main package help page
     pkg_help_header = ""
 
-    if package_name in ['dashHtmlComponents']:
+    if package_name in ["dashHtmlComponents"]:
         pkg_help_header = "Vanilla HTML Components for Dash"
         pkg_help_desc = "Dash is a web application framework that\n\
 provides pure Python and R abstraction around HTML, CSS, and\n\
@@ -513,7 +518,7 @@ def generate_rpkg(pkg_data,
 functions within the dashHtmlComponents package. The\n\
 source for this package is on GitHub:\n\
 plotly/dash-html-components."
-    if package_name in ['dashCoreComponents']:
+    if package_name in ["dashCoreComponents"]:
         pkg_help_header = "Core Interactive UI Components for Dash"
         pkg_help_desc = "Dash ships with supercharged components for\n\
 interactive user interfaces. A core set of components,\n\
@@ -526,22 +531,27 @@ def generate_rpkg(pkg_data,
         package_description=package_description,
         package_version=package_version,
         package_author=package_author,
+        package_depends=package_depends,
+        package_imports=package_imports,
+        package_suggests=package_suggests,
         package_license=package_license,
         package_url=package_url,
         package_issues=package_issues,
-        package_author_no_email=package_author_no_email
+        package_author_no_email=package_author_no_email,
     )
 
-    with open('DESCRIPTION', 'w') as f3:
+    with open("DESCRIPTION", "w") as f3:
         f3.write(description_string)
 
     if pkg_help_header != "":
-        pkghelp = pkghelp_stub.format(package_name=package_name,
-                                      pkg_help_header=pkg_help_header,
-                                      pkg_help_desc=pkg_help_desc,
-                                      lib_name=lib_name,
-                                      package_author=package_author)
-        with open(pkghelp_stub_path, 'w') as f4:
+        pkghelp = pkghelp_stub.format(
+            package_name=package_name,
+            pkg_help_header=pkg_help_header,
+            pkg_help_desc=pkg_help_desc,
+            lib_name=lib_name,
+            package_author=package_author,
+        )
+        with open(pkghelp_stub_path, "w") as f4:
             f4.write(pkghelp)
 
 
@@ -549,23 +559,60 @@ def generate_rpkg(pkg_data,
 # Not required for R package name to be in camel case,
 # but probably more conventional this way
 def snake_case_to_camel_case(namestring):
-    s = namestring.split('_')
-    return s[0] + ''.join(w.capitalize() for w in s[1:])
+    s = namestring.split("_")
+    return s[0] + "".join(w.capitalize() for w in s[1:])
 
 
 # pylint: disable=unused-argument
-def generate_exports(project_shortname,
-                     components,
-                     metadata,
-                     pkg_data,
-                     prefix,
-                     **kwargs):
-    export_string = ''
+def generate_exports(
+        project_shortname,
+        components,
+        metadata,
+        pkg_data,
+        prefix,
+        package_depends,
+        package_imports,
+        package_suggests,
+        **kwargs
+):
+    export_string = ""
     for component in components:
-        if not component.endswith('-*') and \
-                str(component) not in r_keywords and \
-                str(component) not in ['setProps', 'children', 'dashEvents']:
-            export_string += 'export({}{})\n'.format(prefix, component)
+        if (
+                not component.endswith("-*")
+                and str(component) not in r_keywords
+                and str(component) not in ["setProps",
+                                           "children",
+                                           "dashEvents"]
+        ):
+            export_string += "export({}{})\n".format(prefix, component)
+
+    # the following lines enable rudimentary support for bundling in
+    # R functions that are not automatically generated by the transpiler
+    # such that functions contained in the R subdirectory are exported,
+    # so long as they are not in utils.R.
+    rfilelist = []
+    omitlist = ["utils.R", "internal.R"] + [
+        "{}{}.R".format(prefix, component) for component in components
+    ]
+    stripped_line = ""
+    fnlist = []
+
+    for script in os.listdir("R"):
+        if script.endswith(".R") and script not in omitlist:
+            rfilelist += [os.path.join("R", script)]
+
+    # in R, either = or <- may be used to create and assign objects
+    definitions = ["<-function", "=function"]
+
+    for rfile in rfilelist:
+        with open(rfile, "r") as script:
+            for line in script:
+                stripped_line = line.replace(" ", "").replace("\n", "")
+                if any(fndef in stripped_line for fndef in definitions):
+                    fnlist += set([re.split("<-|=", stripped_line)[0]])
+
+    export_string += "\n".join("export({})".format(function)
+                               for function in fnlist)
 
     # now, bundle up the package information and create all the requisite
     # elements of an R package, so that the end result is installable either
@@ -573,5 +620,8 @@ def generate_exports(project_shortname,
     generate_rpkg(
         pkg_data,
         project_shortname,
-        export_string
+        export_string,
+        package_depends,
+        package_imports,
+        package_suggests,
     )
diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py
--- a/dash/development/component_generator.py
+++ b/dash/development/component_generator.py
@@ -19,39 +19,47 @@
 from ._py_components_generation import generate_classes_files
 
 
-class _CombinedFormatter(argparse.ArgumentDefaultsHelpFormatter,
-                         argparse.RawDescriptionHelpFormatter):
+class _CombinedFormatter(
+        argparse.ArgumentDefaultsHelpFormatter,
+        argparse.RawDescriptionHelpFormatter
+):
     pass
 
 
-# pylint: disable=too-many-locals
-def generate_components(components_source, project_shortname,
-                        package_info_filename='package.json',
-                        ignore='^_',
-                        rprefix=None):
+# pylint: disable=too-many-locals, too-many-arguments
+def generate_components(
+        components_source,
+        project_shortname,
+        package_info_filename="package.json",
+        ignore="^_",
+        rprefix=None,
+        rdepends="",
+        rimports="",
+        rsuggests=""
+):
 
-    project_shortname = project_shortname.replace('-', '_').rstrip('/\\')
+    project_shortname = project_shortname.replace("-", "_").rstrip("/\\")
 
     if rprefix:
         prefix = rprefix
 
-    is_windows = sys.platform == 'win32'
+    is_windows = sys.platform == "win32"
 
-    extract_path = pkg_resources.resource_filename('dash', 'extract-meta.js')
+    extract_path = pkg_resources.resource_filename("dash", "extract-meta.js")
 
-    os.environ['NODE_PATH'] = 'node_modules'
+    os.environ["NODE_PATH"] = "node_modules"
     cmd = shlex.split(
-        'node {} {} {}'.format(extract_path, ignore, components_source),
-        posix=not is_windows
+        "node {} {} {}".format(extract_path, ignore, components_source),
+        posix=not is_windows,
     )
 
-    shutil.copyfile('package.json',
-                    os.path.join(project_shortname, package_info_filename))
+    shutil.copyfile(
+        "package.json", os.path.join(project_shortname, package_info_filename)
+    )
 
-    proc = subprocess.Popen(cmd,
-                            stdout=subprocess.PIPE,
-                            stderr=subprocess.PIPE,
-                            shell=is_windows)
+    proc = subprocess.Popen(
+        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
+    )
     out, err = proc.communicate()
     status = proc.poll()
 
@@ -60,9 +68,11 @@ def generate_components(components_source, project_shortname,
 
     if not out:
         print(
-            'Error generating metadata in {} (status={})'.format(
-                project_shortname, status),
-            file=sys.stderr)
+            "Error generating metadata in {} (status={})".format(
+                project_shortname, status
+            ),
+            file=sys.stderr,
+        )
         sys.exit(1)
 
     jsondata_unicode = json.loads(out.decode(), object_pairs_hook=OrderedDict)
@@ -75,26 +85,24 @@ def generate_components(components_source, project_shortname,
     generator_methods = [generate_class_file]
 
     if rprefix:
-        if not os.path.exists('man'):
-            os.makedirs('man')
-        if not os.path.exists('R'):
-            os.makedirs('R')
-        generator_methods.append(
-            functools.partial(write_class_file, prefix=prefix))
-
-    components = generate_classes_files(
-        project_shortname,
-        metadata,
-        *generator_methods
-    )
-
-    with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f:
+        if not os.path.exists("man"):
+            os.makedirs("man")
+        if not os.path.exists("R"):
+            os.makedirs("R")
+        generator_methods.append(functools.partial(write_class_file,
+                                                   prefix=prefix))
+
+    components = generate_classes_files(project_shortname,
+                                        metadata,
+                                        *generator_methods)
+
+    with open(os.path.join(project_shortname, "metadata.json"), "w") as f:
         json.dump(metadata, f, indent=2)
 
     generate_imports(project_shortname, components)
 
     if rprefix:
-        with open('package.json', 'r') as f:
+        with open("package.json", "r") as f:
             jsondata_unicode = json.load(f, object_pairs_hook=OrderedDict)
             if sys.version_info[0] >= 3:
                 pkg_data = jsondata_unicode
@@ -102,60 +110,93 @@ def generate_components(components_source, project_shortname,
                 pkg_data = byteify(jsondata_unicode)
 
         generate_exports(
-            project_shortname, components, metadata, pkg_data, prefix
+            project_shortname,
+            components,
+            metadata,
+            pkg_data,
+            prefix,
+            rdepends,
+            rimports,
+            rsuggests,
         )
 
 
 def cli():
     parser = argparse.ArgumentParser(
-        prog='dash-generate-components',
+        prog="dash-generate-components",
         formatter_class=_CombinedFormatter,
-        description='Generate dash components by extracting the metadata '
-        'using react-docgen. Then map the metadata to python classes.'
+        description="Generate dash components by extracting the metadata "
+        "using react-docgen. Then map the metadata to python classes.",
     )
-    parser.add_argument('components_source',
-                        help='React components source directory.')
+    parser.add_argument("components_source",
+                        help="React components source directory.")
     parser.add_argument(
-        'project_shortname',
-        help='Name of the project to export the classes files.'
+        "project_shortname",
+        help="Name of the project to export the classes files."
     )
     parser.add_argument(
-        '-p', '--package-info-filename',
-        default='package.json',
-        help='The filename of the copied `package.json` to `project_shortname`'
+        "-p",
+        "--package-info-filename",
+        default="package.json",
+        help="The filename of the copied `package.json` \
+to `project_shortname`",
     )
     parser.add_argument(
-        '-i', '--ignore',
-        default='^_',
-        help='Files/directories matching the pattern will be ignored'
+        "-i",
+        "--ignore",
+        default="^_",
+        help="Files/directories matching the pattern will be ignored",
     )
     parser.add_argument(
-        '--r-prefix',
-        help='Experimental: specify a prefix for DashR component names, write'
-             'DashR components to R dir, create R package.'
+        "--r-prefix",
+        help="Specify a prefix for DashR component names, write "
+        "DashR components to R dir, create R package.",
+    )
+    parser.add_argument(
+        "--r-depends",
+        default="",
+        help="Specify a comma-separated list of R packages to be "
+        "inserted into the Depends field of the DESCRIPTION file.",
+    )
+    parser.add_argument(
+        "--r-imports",
+        default="",
+        help="Specify a comma-separated list of R packages to be "
+        "inserted into the Imports field of the DESCRIPTION file.",
+    )
+    parser.add_argument(
+        "--r-suggests",
+        default="",
+        help="Specify a comma-separated list of R packages to be "
+        "inserted into the Suggests field of the DESCRIPTION file.",
     )
 
     args = parser.parse_args()
     generate_components(
-        args.components_source, args.project_shortname,
+        args.components_source,
+        args.project_shortname,
         package_info_filename=args.package_info_filename,
         ignore=args.ignore,
-        rprefix=args.r_prefix)
+        rprefix=args.r_prefix,
+        rdepends=args.r_depends,
+        rimports=args.r_imports,
+        rsuggests=args.r_suggests,
+    )
 
 
 # pylint: disable=undefined-variable
 def byteify(input_object):
     if isinstance(input_object, dict):
-        return OrderedDict([
-            (byteify(key), byteify(value))
-            for key, value in input_object.iteritems()
-        ])
+        return OrderedDict(
+            [(byteify(key), byteify(value)) for key,
+             value in input_object.iteritems()]
+        )
     elif isinstance(input_object, list):
         return [byteify(element) for element in input_object]
     elif isinstance(input_object, unicode):  # noqa:F821
-        return input_object.encode('utf-8')
+        return input_object.encode("utf-8")
     return input_object
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     cli()


Test patch: