Repository: plotly/dash

Issue:
Support arbitrary file extensions
Dash components are packaged with webpack and it's natural to end up having standalone files in webpack bundled projects. For example, static images, fonts or icons would typically not be bundled inside the main bundle.js when they exceed a certain size.

This issue relates to
#96
#480 

Using a combination of ```mimetypes``` as suggested here (https://github.com/plotly/dash/pull/478#discussion_r239234446), explicit overrides (e.g. .map files) and defaulting to no content type (or application/octet-stream) (https://github.com/plotly/dash/pull/478#discussion_r239458219) would allow Dash to expose arbitrary files in a predictable fashion, while providing default behavior for unknowns (some clients will be able to figure out the type when defaulted, some will not)

For known types (mimetypes, map) we could add this additional header
X-Content-Type-Options=nosniff
to prevent the browser from trying to figure out the content type by itself (the browser can sometimes use this to override the default -- content-type)

Additional possible improvement: https://github.com/plotly/dash/issues/480#issuecomment-445039469


Discussion:

Resolving patch:
diff --git a/@plotly/dash-generator-test-component-standard/base/__init__.py b/@plotly/dash-generator-test-component-standard/base/__init__.py
--- a/@plotly/dash-generator-test-component-standard/base/__init__.py
+++ b/@plotly/dash-generator-test-component-standard/base/__init__.py
@@ -16,8 +16,21 @@
     dict(
         relative_package_path='dash_generator_test_component_standard.js',
         namespace='dash_generator_test_component_standard'
+    ),
+    dict(
+        relative_package_path='godfather.ttf',
+        namespace='dash_generator_test_component_standard',
+        dynamic=True
+    )
+]
+
+_css_dist = [
+    dict(
+        relative_package_path='style.css',
+        namespace='dash_generator_test_component_standard'
     )
 ]
 
 for _component in __all__:
     setattr(locals()[_component], '_js_dist', _js_dist)
+    setattr(locals()[_component], '_css_dist', _css_dist)
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -11,6 +11,7 @@
 import threading
 import re
 import logging
+import mimetypes
 
 from functools import wraps
 
@@ -45,6 +46,9 @@
 from . import _validate
 from . import _watch
 
+# Add explicit mapping for map files
+mimetypes.add_type("application/json", ".map", True)
+
 _default_index = """<!DOCTYPE html>
 <html>
     <head>
@@ -669,13 +673,8 @@ def serve_component_suites(self, package_name, fingerprinted_path):
 
         _validate.validate_js_path(self.registered_paths, package_name, path_in_pkg)
 
-        mimetype = (
-            {
-                "js": "application/javascript",
-                "css": "text/css",
-                "map": "application/json",
-            }
-        )[path_in_pkg.split(".")[-1]]
+        extension = "." + path_in_pkg.split(".")[-1]
+        mimetype = mimetypes.types_map.get(extension, "application/octet-stream")
 
         package = sys.modules[package_name]
         self.logger.debug(
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
@@ -505,7 +505,7 @@ def write_js_metadata(pkg_data, project_shortname, has_wildcards):
         for filename in filenames:
             extension = os.path.splitext(filename)[1]
 
-            if extension not in [".css", ".js", ".map"]:
+            if extension in [".py", ".pyc", ".json"]:
                 continue
 
             target_dirname = os.path.join(


Test patch:
diff --git a/tests/integration/test_generation.py b/tests/integration/test_generation.py
--- a/tests/integration/test_generation.py
+++ b/tests/integration/test_generation.py
@@ -1,7 +1,12 @@
 from dash import Dash
+from dash.dependencies import Input, Output
+from dash.exceptions import PreventUpdate
+
 from dash_generator_test_component_nested import MyNestedComponent
 from dash_generator_test_component_standard import MyStandardComponent
-from dash_html_components import Div
+from dash_html_components import Button, Div
+
+from selenium.webdriver.support.ui import WebDriverWait
 
 
 def test_gene001_simple_callback(dash_duo):
@@ -18,3 +23,39 @@ def test_gene001_simple_callback(dash_duo):
 
     assert dash_duo.wait_for_element("#standard").text == "Standard"
     assert dash_duo.wait_for_element("#nested").text == "Nested"
+
+    dash_duo.percy_snapshot(name="gene001-simple-callback")
+
+
+def test_gene002_arbitrary_resources(dash_duo):
+    app = Dash(__name__)
+
+    app.layout = Div([Button(id="btn"), Div(id="container")])
+
+    @app.callback(Output("container", "children"), [Input("btn", "n_clicks")])
+    def update_container(n_clicks):
+        if n_clicks is None:
+            raise PreventUpdate
+
+        return MyStandardComponent(
+            id="standard", value="Standard", style={"font-family": "godfather"}
+        )
+
+    dash_duo.start_server(app)
+
+    assert (
+        dash_duo.driver.execute_script("return document.fonts.check('1em godfather')")
+        is False
+    )
+
+    dash_duo.wait_for_element("#btn").click()
+    assert dash_duo.wait_for_element("#standard").text == "Standard"
+
+    WebDriverWait(dash_duo.driver, 10).until(
+        lambda _: dash_duo.driver.execute_script(
+            "return document.fonts.check('1em godfather')"
+        )
+        is True,
+    )
+
+    dash_duo.percy_snapshot(name="gene002-arbitrary-resource")