script type=module support, closes #1186

This commit is contained in:
Simon Willison 2021-01-13 17:50:52 -08:00
commit fa0c3777b8
6 changed files with 74 additions and 24 deletions

View file

@ -864,19 +864,23 @@ class Datasette:
if isinstance(url_or_dict, dict): if isinstance(url_or_dict, dict):
url = url_or_dict["url"] url = url_or_dict["url"]
sri = url_or_dict.get("sri") sri = url_or_dict.get("sri")
module = bool(url_or_dict.get("module"))
else: else:
url = url_or_dict url = url_or_dict
sri = None sri = None
module = False
if url in seen_urls: if url in seen_urls:
continue continue
seen_urls.add(url) seen_urls.add(url)
if url.startswith("/"): if url.startswith("/"):
# Take base_url into account: # Take base_url into account:
url = self.urls.path(url) url = self.urls.path(url)
script = {"url": url}
if sri: if sri:
output.append({"url": url, "sri": sri}) script["sri"] = sri
else: if module:
output.append({"url": url}) script["module"] = True
output.append(script)
return output return output
def app(self): def app(self):

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}> <link rel="stylesheet" href="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}>
{% endfor %} {% endfor %}
{% for url in extra_js_urls %} {% for url in extra_js_urls %}
<script src="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script> <script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
{% endfor %} {% endfor %}
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
</head> </head>

View file

@ -5,6 +5,8 @@ Custom pages and templates
Datasette provides a number of ways of customizing the way data is displayed. Datasette provides a number of ways of customizing the way data is displayed.
.. _customization_css_and_javascript:
Custom CSS and JavaScript Custom CSS and JavaScript
------------------------- -------------------------
@ -25,7 +27,12 @@ Your ``metadata.json`` file can include links that look like this:
] ]
} }
The extra CSS and JavaScript files will be linked in the ``<head>`` of every page. The extra CSS and JavaScript files will be linked in the ``<head>`` of every page:
.. code-block:: html
<link rel="stylesheet" href="https://simonwillison.net/static/css/all.bf8cd891642c.css">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
You can also specify a SRI (subresource integrity hash) for these assets: You can also specify a SRI (subresource integrity hash) for these assets:
@ -46,9 +53,39 @@ You can also specify a SRI (subresource integrity hash) for these assets:
] ]
} }
This will produce:
.. code-block:: html
<link rel="stylesheet" href="https://simonwillison.net/static/css/all.bf8cd891642c.css"
integrity="sha384-9qIZekWUyjCyDIf2YK1FRoKiPJq4PHt6tp/ulnuuyRBvazd0hG7pWbE99zvwSznI"
crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
crossorigin="anonymous"></script>
Modern browsers will only execute the stylesheet or JavaScript if the SRI hash Modern browsers will only execute the stylesheet or JavaScript if the SRI hash
matches the content served. You can generate hashes using `www.srihash.org <https://www.srihash.org/>`_ matches the content served. You can generate hashes using `www.srihash.org <https://www.srihash.org/>`_
Items in ``"extra_js_urls"`` can specify ``"module": true`` if they reference JavaScript that uses `JavaScript modules <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules>`__. This configuration:
.. code-block:: json
{
"extra_js_urls": [
{
"url": "https://example.datasette.io/module.js",
"module": true
}
]
}
Will produce this HTML:
.. code-block:: html
<script type="module" src="https://example.datasette.io/module.js"></script>
CSS classes on the <body> CSS classes on the <body>
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -182,7 +182,7 @@ This can be a list of URLs:
@hookimpl @hookimpl
def extra_css_urls(): def extra_css_urls():
return [ return [
'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css' "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
] ]
Or a list of dictionaries defining both a URL and an Or a list of dictionaries defining both a URL and an
@ -190,21 +190,17 @@ Or a list of dictionaries defining both a URL and an
.. code-block:: python .. code-block:: python
from datasette import hookimpl
@hookimpl @hookimpl
def extra_css_urls(): def extra_css_urls():
return [{ return [{
'url': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css', "url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css",
'sri': 'sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4', "sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4",
}] }]
This function can also return an awaitable function, useful if it needs to run any async code: This function can also return an awaitable function, useful if it needs to run any async code:
.. code-block:: python .. code-block:: python
from datasette import hookimpl
@hookimpl @hookimpl
def extra_css_urls(datasette): def extra_css_urls(datasette):
async def inner(): async def inner():
@ -233,8 +229,8 @@ return a list of URLs, a list of dictionaries or an awaitable function that retu
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [{ return [{
'url': 'https://code.jquery.com/jquery-3.3.1.slim.min.js', "url": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
'sri': 'sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo', "sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
}] }]
You can also return URLs to files from your plugin's ``static/`` directory, if You can also return URLs to files from your plugin's ``static/`` directory, if
@ -242,12 +238,21 @@ you have one:
.. code-block:: python .. code-block:: python
from datasette import hookimpl
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [ return [
'/-/static-plugins/your-plugin/app.js' "/-/static-plugins/your-plugin/app.js"
]
If your code uses `JavaScript modules <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules>`__ you should include the ``"module": True`` key. See :ref:`customization_css_and_javascript` for more details.
.. code-block:: python
@hookimpl
def extra_js_urls():
return [{
"url": "/-/static-plugins/your-plugin/app.js",
"module": True
] ]
Examples: `datasette-cluster-map <https://github.com/simonw/datasette-cluster-map>`_, `datasette-vega <https://github.com/simonw/datasette-vega>`_ Examples: `datasette-cluster-map <https://github.com/simonw/datasette-cluster-map>`_, `datasette-vega <https://github.com/simonw/datasette-vega>`_

View file

@ -61,6 +61,7 @@ def extra_js_urls():
"sri": "SRIHASH", "sri": "SRIHASH",
}, },
"https://plugin-example.datasette.io/plugin1.js", "https://plugin-example.datasette.io/plugin1.js",
{"url": "https://plugin-example.datasette.io/plugin.module.js", "module": True},
] ]

View file

@ -118,16 +118,19 @@ def test_hook_extra_css_urls(app_client, path, expected_decoded_object):
def test_hook_extra_js_urls(app_client): def test_hook_extra_js_urls(app_client):
response = app_client.get("/") response = app_client.get("/")
scripts = Soup(response.body, "html.parser").findAll("script") scripts = Soup(response.body, "html.parser").findAll("script")
assert [ script_attrs = [s.attrs for s in scripts]
s for attrs in [
for s in scripts {
if s.attrs
== {
"integrity": "SRIHASH", "integrity": "SRIHASH",
"crossorigin": "anonymous", "crossorigin": "anonymous",
"src": "https://plugin-example.datasette.io/jquery.js", "src": "https://plugin-example.datasette.io/jquery.js",
} },
] {
"src": "https://plugin-example.datasette.io/plugin.module.js",
"type": "module",
},
]:
assert any(s == attrs for s in script_attrs), "Expected: {}".format(attrs)
def test_plugins_with_duplicate_js_urls(app_client): def test_plugins_with_duplicate_js_urls(app_client):