From 3ba3822d8ed1ebc865575a7e2acad0b8724cc01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 22 Oct 2019 19:51:34 -0400 Subject: [PATCH 01/19] add use of etag / if-none-match --- dash/dash.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 0c402b2108..a51f027a61 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -711,10 +711,19 @@ def serve_component_suites(self, package_name, path_in_package_dist): package.__path__, ) - return flask.Response( + response=flask.Response( pkgutil.get_data(package_name, path_in_package_dist), mimetype=mimetype, ) + response.add_etag() + (tag, weak) = response.get_etag() + + request_etag = flask.request.headers.get('If-None-Match') + + if '"{}"'.format(tag) == request_etag: + response = flask.Response(None, status = 304) + + return response def index(self, *args, **kwargs): # pylint: disable=unused-argument scripts = self._generate_scripts_html() From 608c0d73359043d926ce43c3c2b4cb0f9fb4cab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 22 Oct 2019 19:58:48 -0400 Subject: [PATCH 02/19] black --- dash/dash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index a51f027a61..45cc31342a 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -711,7 +711,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): package.__path__, ) - response=flask.Response( + response = flask.Response( pkgutil.get_data(package_name, path_in_package_dist), mimetype=mimetype, ) @@ -721,7 +721,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): request_etag = flask.request.headers.get('If-None-Match') if '"{}"'.format(tag) == request_etag: - response = flask.Response(None, status = 304) + response = flask.Response(None, status=304) return response From d76804dbefae18f159dbbb5e89a929cddbb4fa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 22 Oct 2019 20:08:26 -0400 Subject: [PATCH 03/19] handling tuples --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 45cc31342a..594487d184 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -716,7 +716,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): mimetype=mimetype, ) response.add_etag() - (tag, weak) = response.get_etag() + (tag,) = response.get_etag()[:1] request_etag = flask.request.headers.get('If-None-Match') From be2360031aaf14e89a841ba9b5bba6bfd4707199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 14:22:14 -0400 Subject: [PATCH 04/19] etag or fingerprint --- .../webpack-dash-dynamic-import/package.json | 2 +- .../webpack-dash-dynamic-import/src/index.js | 27 +++++++++++-- dash/dash.py | 39 +++++++++++++++---- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/@plotly/webpack-dash-dynamic-import/package.json b/@plotly/webpack-dash-dynamic-import/package.json index 8a0ec41b5f..789b1e6d0e 100644 --- a/@plotly/webpack-dash-dynamic-import/package.json +++ b/@plotly/webpack-dash-dynamic-import/package.json @@ -1,6 +1,6 @@ { "name": "@plotly/webpack-dash-dynamic-import", - "version": "1.0.0", + "version": "1.1.0", "description": "Webpack Plugin for Dynamic Import in Dash", "repository": { "type": "git", diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js index 3f930a3298..012c12cf53 100644 --- a/@plotly/webpack-dash-dynamic-import/src/index.js +++ b/@plotly/webpack-dash-dynamic-import/src/index.js @@ -1,4 +1,16 @@ -const resolveImportSource = `\ +const fs = require('fs'); + +function getFingerprint() { + const package = fs.readFileSync('./package.json'); + const packageJson = JSON.parse(package); + + const timestamp = Math.round(Date.now() / 1000); + const version = packageJson.version.replace(/[.]/g, '_'); + + return `"v${version}m${timestamp}"`; +} + +const resolveImportSource = () => `\ Object.defineProperty(__webpack_require__, 'p', { get: (function () { let script = document.currentScript; @@ -15,7 +27,16 @@ Object.defineProperty(__webpack_require__, 'p', { return url; }; })() -});` +}); + +const __jsonpScriptSrc__ = jsonpScriptSrc; +jsonpScriptSrc = function(chunkId) { + const srcFragments = __jsonpScriptSrc__(chunkId).split('.'); + srcFragments.splice(-1, 0, ${getFingerprint()}); + + return srcFragments.join('.'); +} +` class WebpackDashDynamicImport { apply(compiler) { @@ -23,7 +44,7 @@ class WebpackDashDynamicImport { compilation.mainTemplate.hooks.requireExtensions.tap('WebpackDashDynamicImport > RequireExtensions', (source, chunk, hash) => { return [ source, - resolveImportSource + resolveImportSource() ] }); }); diff --git a/dash/dash.py b/dash/dash.py index 594487d184..bc991633f0 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -541,12 +541,15 @@ def _relative_url_path(relative_package_path="", namespace=""): modified = int(os.stat(module_path).st_mtime) - return "{}_dash-component-suites/{}/{}?v={}&m={}".format( + return "{}_dash-component-suites/{}/{}.v{}m{}.{}".format( self.config.requests_pathname_prefix, namespace, - relative_package_path, - importlib.import_module(namespace).__version__, + '.'.join(relative_package_path.split('.')[:-1]), + importlib.import_module(namespace).__version__.replace( + '.', '_' + ), modified, + '.'.join(relative_package_path.split('.')[-1:]), ) srcs = [] @@ -676,6 +679,18 @@ def _generate_meta_html(self): # Serve the JS bundles for each package def serve_component_suites(self, package_name, path_in_package_dist): + # Check if the resource has a fingerprint + res = re.match( + "^(.*)[.]v\d+_\d+_\d+(-\w+[.]?\d+)?m\d{10}([.]js)$", + path_in_package_dist, + ) + + fingerprint = res is not None + # Resolve real resource name from fingerprinted resource path + path_in_package_dist = ( + res[1] + res[3] if res is not None else path_in_package_dist + ) + if package_name not in self.registered_paths: raise exceptions.DependencyException( "Error loading dependency.\n" @@ -715,13 +730,21 @@ def serve_component_suites(self, package_name, path_in_package_dist): pkgutil.get_data(package_name, path_in_package_dist), mimetype=mimetype, ) - response.add_etag() - (tag,) = response.get_etag()[:1] - request_etag = flask.request.headers.get('If-None-Match') + if fingerprint: + # Fingerprinted resources are good forever (1 year) + # No need for ETag as the fingerprint changes with each build + response.cache_control.max_age = 31536000 # 1 year + else: + # Non-fingerprinted resources are given an ETag that + # will be used / check on future requests + response.add_etag() + tag = response.get_etag()[:1][0] + + request_etag = flask.request.headers.get('If-None-Match') - if '"{}"'.format(tag) == request_etag: - response = flask.Response(None, status=304) + if '"{}"'.format(tag) == request_etag: + response = flask.Response(None, status=304) return response From 9c3f2fe8f3b83ef749db077bec2e95e66b5efe18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 14:31:25 -0400 Subject: [PATCH 05/19] lint --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index bc991633f0..b83ddef31d 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -681,7 +681,7 @@ def _generate_meta_html(self): def serve_component_suites(self, package_name, path_in_package_dist): # Check if the resource has a fingerprint res = re.match( - "^(.*)[.]v\d+_\d+_\d+(-\w+[.]?\d+)?m\d{10}([.]js)$", + r'^(.*)[.]v\d+_\d+_\d+(-\w+[.]?\d+)?m\d{10}([.]js)$', path_in_package_dist, ) From daf8d63cf7d04ce72d1b9cab9b652940621b8774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 14:38:37 -0400 Subject: [PATCH 06/19] update resource tests --- tests/unit/test_resources.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 448c5e030e..3d3727b485 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -37,7 +37,7 @@ def test_external(mocker): mocker.patch("dash_core_components._js_dist") mocker.patch("dash_html_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212 - dcc.__version__ = 1 + dcc.__version__ = "1.0.0" app = dash.Dash( __name__, assets_folder="tests/assets", assets_ignore="load_after.+.js" @@ -66,7 +66,7 @@ def test_internal(mocker): mocker.patch("dash_core_components._js_dist") mocker.patch("dash_html_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, - dcc.__version__ = 1 + dcc.__version__ = "1.0.0" app = dash.Dash( __name__, assets_folder="tests/assets", assets_ignore="load_after.+.js" @@ -83,10 +83,10 @@ def test_internal(mocker): assert resource == [ "/_dash-component-suites/" - "dash_core_components/external_javascript.js?v=1&m=1", + "dash_core_components/external_javascript.v1_0_0m1.js", "/_dash-component-suites/" - "dash_core_components/external_css.css?v=1&m=1", - "/_dash-component-suites/" "dash_core_components/fake_dcc.js?v=1&m=1", + "dash_core_components/external_css.v1_0_0m1.css", + "/_dash-component-suites/" "dash_core_components/fake_dcc.v1_0_0m1.js", ] assert ( From aa5e8f70fef5f68b4b7c2f354074c844bddbf9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 15:23:32 -0400 Subject: [PATCH 07/19] py2.7 --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index b83ddef31d..71956770a1 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -688,7 +688,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): fingerprint = res is not None # Resolve real resource name from fingerprinted resource path path_in_package_dist = ( - res[1] + res[3] if res is not None else path_in_package_dist + res.group(1) + res.group(3) if res is not None else path_in_package_dist ) if package_name not in self.registered_paths: From 61fe077bda13451eab9c8141a6b43c845201b07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 15:24:05 -0400 Subject: [PATCH 08/19] lint --- dash/dash.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 71956770a1..3c2425ac1d 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -688,7 +688,9 @@ def serve_component_suites(self, package_name, path_in_package_dist): fingerprint = res is not None # Resolve real resource name from fingerprinted resource path path_in_package_dist = ( - res.group(1) + res.group(3) if res is not None else path_in_package_dist + res.group(1) + res.group(3) + if res is not None + else path_in_package_dist ) if package_name not in self.registered_paths: From 8e6e2f2c1eaf8358bec799af78251223d4807f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 15:38:12 -0400 Subject: [PATCH 09/19] clean up --- dash/dash.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 3c2425ac1d..c95edc05bd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -685,7 +685,6 @@ def serve_component_suites(self, package_name, path_in_package_dist): path_in_package_dist, ) - fingerprint = res is not None # Resolve real resource name from fingerprinted resource path path_in_package_dist = ( res.group(1) + res.group(3) @@ -733,7 +732,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): mimetype=mimetype, ) - if fingerprint: + if res is not None: # Fingerprinted resources are good forever (1 year) # No need for ETag as the fingerprint changes with each build response.cache_control.max_age = 31536000 # 1 year From 5e61384996035ff47542134cf5e2a8ad978c1a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 15:40:11 -0400 Subject: [PATCH 10/19] clean up --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index c95edc05bd..f47cbd5f2f 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -740,7 +740,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): # Non-fingerprinted resources are given an ETag that # will be used / check on future requests response.add_etag() - tag = response.get_etag()[:1][0] + tag = response.get_etag()[0] request_etag = flask.request.headers.get('If-None-Match') From 193304620b35e49c57217139c9ad2918dcbc0bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 23 Oct 2019 15:59:23 -0400 Subject: [PATCH 11/19] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287cd196a6..e3240969e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added - [#964](https://github.com/plotly/dash/pull/964) Adds support for preventing -updates in clientside functions. +updates in clientside functions. - Reject all updates with `throw window.dash_clientside.PreventUpdate;` - Reject a single output by returning `window.dash_clientside.no_update` +- [#973](https://github.com/plotly/dash/pull/973) Adds support for resource caching and adds a fallback caching mechanism through etag ## [1.4.1] - 2019-10-17 ### Fixed From c16d0695858d1e7e202252e4b13c0e02e689b0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 24 Oct 2019 10:50:46 -0400 Subject: [PATCH 12/19] - improve fingerprinting - improve recognizing fingerprints - add fingerprint tests --- .../webpack-dash-dynamic-import/src/index.js | 2 +- dash/dash.py | 27 ++++------ dash/fingerprint.py | 31 +++++++++++ tests/unit/test_fingerprint.py | 52 +++++++++++++++++++ 4 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 dash/fingerprint.py create mode 100644 tests/unit/test_fingerprint.py diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js index 012c12cf53..e038b2fe47 100644 --- a/@plotly/webpack-dash-dynamic-import/src/index.js +++ b/@plotly/webpack-dash-dynamic-import/src/index.js @@ -32,7 +32,7 @@ Object.defineProperty(__webpack_require__, 'p', { const __jsonpScriptSrc__ = jsonpScriptSrc; jsonpScriptSrc = function(chunkId) { const srcFragments = __jsonpScriptSrc__(chunkId).split('.'); - srcFragments.splice(-1, 0, ${getFingerprint()}); + srcFragments.splice(1, 0, ${getFingerprint()}); return srcFragments.join('.'); } diff --git a/dash/dash.py b/dash/dash.py index f47cbd5f2f..36a5f39126 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -24,6 +24,7 @@ import dash_renderer from .dependencies import Input, Output, State +from .fingerprint import build_fingerprint, check_fingerprint from .resources import Scripts, Css from .development.base_component import Component, ComponentRegistry from . import exceptions @@ -541,15 +542,14 @@ def _relative_url_path(relative_package_path="", namespace=""): modified = int(os.stat(module_path).st_mtime) - return "{}_dash-component-suites/{}/{}.v{}m{}.{}".format( + return "{}_dash-component-suites/{}/{}".format( self.config.requests_pathname_prefix, namespace, - '.'.join(relative_package_path.split('.')[:-1]), - importlib.import_module(namespace).__version__.replace( - '.', '_' + build_fingerprint( + relative_package_path, + importlib.import_module(namespace).__version__, + modified, ), - modified, - '.'.join(relative_package_path.split('.')[-1:]), ) srcs = [] @@ -679,17 +679,8 @@ def _generate_meta_html(self): # Serve the JS bundles for each package def serve_component_suites(self, package_name, path_in_package_dist): - # Check if the resource has a fingerprint - res = re.match( - r'^(.*)[.]v\d+_\d+_\d+(-\w+[.]?\d+)?m\d{10}([.]js)$', - path_in_package_dist, - ) - - # Resolve real resource name from fingerprinted resource path - path_in_package_dist = ( - res.group(1) + res.group(3) - if res is not None - else path_in_package_dist + (path_in_package_dist, has_fingerprint) = check_fingerprint( + path_in_package_dist ) if package_name not in self.registered_paths: @@ -732,7 +723,7 @@ def serve_component_suites(self, package_name, path_in_package_dist): mimetype=mimetype, ) - if res is not None: + if has_fingerprint: # Fingerprinted resources are good forever (1 year) # No need for ETag as the fingerprint changes with each build response.cache_control.max_age = 31536000 # 1 year diff --git a/dash/fingerprint.py b/dash/fingerprint.py new file mode 100644 index 0000000000..987e3f41d9 --- /dev/null +++ b/dash/fingerprint.py @@ -0,0 +1,31 @@ +import re + +build_regex = re.compile(r'^(?P[\w@-]+)(?P.*)$') + +check_regex = re.compile( + r'^(?P.*)[.]v[\w-]+m[0-9a-fA-F]+(?P(?:(?:(? Date: Thu, 24 Oct 2019 10:55:57 -0400 Subject: [PATCH 13/19] update handling of `@` in fingerprinting and test (for react@xx.yy.zz resource and the likes) --- dash/fingerprint.py | 2 +- tests/unit/test_fingerprint.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/fingerprint.py b/dash/fingerprint.py index 987e3f41d9..dc6a3be006 100644 --- a/dash/fingerprint.py +++ b/dash/fingerprint.py @@ -3,7 +3,7 @@ build_regex = re.compile(r'^(?P[\w@-]+)(?P.*)$') check_regex = re.compile( - r'^(?P.*)[.]v[\w-]+m[0-9a-fA-F]+(?P(?:(?:(?.*)[.]v[\w-]+m[0-9a-fA-F]+(?P(?:(?:(? Date: Thu, 24 Oct 2019 11:01:56 -0400 Subject: [PATCH 14/19] add tests for version and hash --- dash/fingerprint.py | 4 ++-- tests/unit/test_fingerprint.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dash/fingerprint.py b/dash/fingerprint.py index dc6a3be006..5d0c587310 100644 --- a/dash/fingerprint.py +++ b/dash/fingerprint.py @@ -7,13 +7,13 @@ ) -def build_fingerprint(path, version, modified): +def build_fingerprint(path, version, hash): res = build_regex.match(path) return '{}.v{}m{}{}'.format( res.group('filename'), str(version).replace('.', '_'), - modified, + hash, res.group('extension'), ) diff --git a/tests/unit/test_fingerprint.py b/tests/unit/test_fingerprint.py index 8bb233b5e2..cf9a8f87dd 100644 --- a/tests/unit/test_fingerprint.py +++ b/tests/unit/test_fingerprint.py @@ -2,10 +2,12 @@ from dash.fingerprint import build_fingerprint, check_fingerprint version = 1 -modified = 1 +hash = 1 valid_resources = [ {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1m1.8.6.min.js'}, + {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1', 'hash': '1234567890abcdef' }, + {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1-alpha_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1-alpha.1', 'hash': '1234567890abcdef' }, {'path':'dash.plotly.js', 'fingerprint': 'dash.v1m1.plotly.js'}, {'path':'dash.plotly.j_s', 'fingerprint': 'dash.v1m1.plotly.j_s'}, {'path':'dash.plotly.css', 'fingerprint': 'dash.v1m1.plotly.css'}, @@ -35,7 +37,7 @@ def test_fingerprint(): for resource in valid_resources: # The fingerprint matches expectations - fingerprint = build_fingerprint(resource.get('path'), version, modified) + fingerprint = build_fingerprint(resource.get('path'), resource.get('version', version), resource.get('hash', hash)) assert fingerprint == resource.get('fingerprint') (original_path, has_fingerprint) = check_fingerprint(fingerprint) From e0847f5b6476ea47b6fff5c42990e3184229d091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 24 Oct 2019 11:03:29 -0400 Subject: [PATCH 15/19] clean up --- tests/unit/test_fingerprint.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_fingerprint.py b/tests/unit/test_fingerprint.py index cf9a8f87dd..7c80d7d7b3 100644 --- a/tests/unit/test_fingerprint.py +++ b/tests/unit/test_fingerprint.py @@ -5,13 +5,13 @@ hash = 1 valid_resources = [ - {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1m1.8.6.min.js'}, - {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1', 'hash': '1234567890abcdef' }, - {'path':'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1-alpha_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1-alpha.1', 'hash': '1234567890abcdef' }, - {'path':'dash.plotly.js', 'fingerprint': 'dash.v1m1.plotly.js'}, - {'path':'dash.plotly.j_s', 'fingerprint': 'dash.v1m1.plotly.j_s'}, - {'path':'dash.plotly.css', 'fingerprint': 'dash.v1m1.plotly.css'}, - {'path':'dash.plotly.xxx.yyy.zzz', 'fingerprint': 'dash.v1m1.plotly.xxx.yyy.zzz'} + {'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1m1.8.6.min.js'}, + {'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1', 'hash': '1234567890abcdef' }, + {'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1-alpha_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1-alpha.1', 'hash': '1234567890abcdef' }, + {'path': 'dash.plotly.js', 'fingerprint': 'dash.v1m1.plotly.js'}, + {'path': 'dash.plotly.j_s', 'fingerprint': 'dash.v1m1.plotly.j_s'}, + {'path': 'dash.plotly.css', 'fingerprint': 'dash.v1m1.plotly.css'}, + {'path': 'dash.plotly.xxx.yyy.zzz', 'fingerprint': 'dash.v1m1.plotly.xxx.yyy.zzz'} ] valid_fingerprints = [ From d18053316159730957b29bf56c014ec0d78fb26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 24 Oct 2019 11:06:11 -0400 Subject: [PATCH 16/19] hash --- dash/fingerprint.py | 4 ++-- tests/unit/test_fingerprint.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dash/fingerprint.py b/dash/fingerprint.py index 5d0c587310..0c67e31ea5 100644 --- a/dash/fingerprint.py +++ b/dash/fingerprint.py @@ -7,13 +7,13 @@ ) -def build_fingerprint(path, version, hash): +def build_fingerprint(path, version, hash_value): res = build_regex.match(path) return '{}.v{}m{}{}'.format( res.group('filename'), str(version).replace('.', '_'), - hash, + hash_value, res.group('extension'), ) diff --git a/tests/unit/test_fingerprint.py b/tests/unit/test_fingerprint.py index 7c80d7d7b3..bdc64b797c 100644 --- a/tests/unit/test_fingerprint.py +++ b/tests/unit/test_fingerprint.py @@ -2,7 +2,7 @@ from dash.fingerprint import build_fingerprint, check_fingerprint version = 1 -hash = 1 +hash_value = 1 valid_resources = [ {'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1m1.8.6.min.js'}, @@ -37,7 +37,7 @@ def test_fingerprint(): for resource in valid_resources: # The fingerprint matches expectations - fingerprint = build_fingerprint(resource.get('path'), resource.get('version', version), resource.get('hash', hash)) + fingerprint = build_fingerprint(resource.get('path'), resource.get('version', version), resource.get('hash', hash_value)) assert fingerprint == resource.get('fingerprint') (original_path, has_fingerprint) = check_fingerprint(fingerprint) From c71955e191cb7b8d0564a8d33bb7cf83922566b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 24 Oct 2019 15:25:52 -0400 Subject: [PATCH 17/19] fix error adding fingerprint to url --- @plotly/webpack-dash-dynamic-import/src/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js index e038b2fe47..302f3edbb6 100644 --- a/@plotly/webpack-dash-dynamic-import/src/index.js +++ b/@plotly/webpack-dash-dynamic-import/src/index.js @@ -31,10 +31,13 @@ Object.defineProperty(__webpack_require__, 'p', { const __jsonpScriptSrc__ = jsonpScriptSrc; jsonpScriptSrc = function(chunkId) { - const srcFragments = __jsonpScriptSrc__(chunkId).split('.'); - srcFragments.splice(1, 0, ${getFingerprint()}); + const srcFragments = __jsonpScriptSrc__(chunkId).split('/'); + const fileFragments = srcFragments.slice(-1)[0].split('.'); - return srcFragments.join('.'); + fileFragments.splice(1, 0, ${getFingerprint()}); + srcFragments.splice(-1, 1, fileFragments.join('.')) + + return srcFragments.join('/'); } ` From 0e091f1e3ee50ba02b8f6cf86ce637feed55339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Mon, 28 Oct 2019 09:23:03 -0400 Subject: [PATCH 18/19] Update dash/dash.py Don't need the parens Co-Authored-By: alexcjohnson --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 36a5f39126..ddb64bec80 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -679,7 +679,7 @@ def _generate_meta_html(self): # Serve the JS bundles for each package def serve_component_suites(self, package_name, path_in_package_dist): - (path_in_package_dist, has_fingerprint) = check_fingerprint( + path_in_package_dist, has_fingerprint = check_fingerprint( path_in_package_dist ) From f156edeaf6641539465464801bd46b1b308a68dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 28 Oct 2019 13:11:21 -0400 Subject: [PATCH 19/19] plugin - distinguish local from remote script resources --- .../webpack-dash-dynamic-import/src/index.js | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js index 302f3edbb6..5a1dc014ca 100644 --- a/@plotly/webpack-dash-dynamic-import/src/index.js +++ b/@plotly/webpack-dash-dynamic-import/src/index.js @@ -11,15 +11,25 @@ function getFingerprint() { } const resolveImportSource = () => `\ +const getCurrentScript = function() { + let script = document.currentScript; + if (!script) { + /* Shim for IE11 and below */ + /* Do not take into account async scripts and inline scripts */ + const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; }); + script = scripts.slice(-1)[0]; + } + + return script; +}; + +const isLocalScript = function(script) { + return /\/_dash-components-suite\//.test(script.src); +}; + Object.defineProperty(__webpack_require__, 'p', { get: (function () { - let script = document.currentScript; - if (!script) { - /* Shim for IE11 and below */ - /* Do not take into account async scripts and inline scripts */ - const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; }); - script = scripts.slice(-1)[0]; - } + let script = getCurrentScript(); var url = script.src.split('/').slice(0, -1).join('/') + '/'; @@ -31,14 +41,23 @@ Object.defineProperty(__webpack_require__, 'p', { const __jsonpScriptSrc__ = jsonpScriptSrc; jsonpScriptSrc = function(chunkId) { - const srcFragments = __jsonpScriptSrc__(chunkId).split('/'); + let script = getCurrentScript(); + let isLocal = isLocalScript(script); + + let src = __jsonpScriptSrc__(chunkId); + + if(!isLocal) { + return src; + } + + const srcFragments = src.split('/'); const fileFragments = srcFragments.slice(-1)[0].split('.'); fileFragments.splice(1, 0, ${getFingerprint()}); srcFragments.splice(-1, 1, fileFragments.join('.')) return srcFragments.join('/'); -} +}; ` class WebpackDashDynamicImport {