diff --git a/setup.cfg b/setup.cfg index ddb7da9e..08aedd7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description_file = README.rst +description_file = README.md diff --git a/setup.py b/setup.py index 8029f869..ca6b28ad 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import os import re -from setuptools import setup +from setuptools import find_packages, setup def rel(*parts): @@ -9,7 +9,7 @@ def rel(*parts): return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts)) -with open("README.md", "r") as handler: +with open(rel("README.md"), "r", encoding="utf-8") as handler: README = handler.read() with open(rel("webpack_loader", "__init__.py")) as handler: @@ -20,11 +20,7 @@ def rel(*parts): setup( name="django-webpack-loader", - packages=[ - "webpack_loader", - "webpack_loader/templatetags", - "webpack_loader/contrib", - ], + packages=find_packages(), version=VERSION, license="MIT License", description="Transparently use webpack with django", diff --git a/tests/app/tests/test_webpack.py b/tests/app/tests/test_webpack.py index 383eefaf..4ff080ff 100644 --- a/tests/app/tests/test_webpack.py +++ b/tests/app/tests/test_webpack.py @@ -23,6 +23,7 @@ WebpackLoaderBadStatsError, WebpackLoaderTimeoutError, ) +from webpack_loader.loaders import WebpackLoader from webpack_loader.templatetags.webpack_loader import _WARNING_MESSAGE from webpack_loader.utils import get_as_tags, get_loader, get_as_url_to_tag_dict @@ -158,35 +159,37 @@ def test_templatetags(self): view = TemplateView.as_view(template_name='home.html') request = self.factory.get('/') result = view(request) + rendered_content = result.rendered_content self.assertIn(( ''), - result.rendered_content) + 'rel="stylesheet"/>'), + rendered_content) self.assertIn(( ''), result.rendered_content) + 'async charset="UTF-8">'), rendered_content) self.assertIn(( ''), result.rendered_content) + 'rel="stylesheet"/>'), rendered_content) self.assertIn(( - ''), result.rendered_content) + ''), rendered_content) self.assertIn( - '', result.rendered_content) + '', rendered_content) - self.assertIn('
  • All from getFiles already rendered
  • ', result.rendered_content) + self.assertIn('
  • All from getFiles already rendered
  • ', rendered_content) request = self.factory.get('/') view = TemplateView.as_view(template_name='only_files.html') result = view(request) + rendered_content = result.rendered_content self.assertIn(( "var contentCss = " "'/static/django_webpack_loader_bundles/main.css'"), - result.rendered_content) + rendered_content) self.assertIn( "var contentJS = '/static/django_webpack_loader_bundles/main.js'", - result.rendered_content) + rendered_content) self.compile_bundles('webpack.config.publicPath.js') request = self.factory.get('/') @@ -202,23 +205,24 @@ def test_preload(self): view = TemplateView.as_view(template_name='preload.html') request = self.factory.get('/') result = view(request) + rendered_content = result.rendered_content # Preload self.assertIn(( ''), result.rendered_content) + 'rel="preload" as="style"/>'), rendered_content) self.assertIn(( ''), - result.rendered_content) + 'django_webpack_loader_bundles/main.js"/>'), + rendered_content) # Resources self.assertIn(( ''), result.rendered_content) + 'rel="stylesheet"/>'), rendered_content) self.assertIn(( - ''), result.rendered_content) + ''), rendered_content) def test_integrity(self): self.compile_bundles('webpack.config.integrity.js') @@ -228,22 +232,23 @@ def test_integrity(self): view = TemplateView.as_view(template_name='single.html') request = self.factory.get('/') result = view(request) + rendered_content = result.rendered_content self.assertIn(( ''), result.rendered_content) + 'Pp35WlFR7ykkIafUG8cma4vbEfheH1NVbjsON5BHm8U13I4g==">' + ''), rendered_content) self.assertIn(( ''), - result.rendered_content + 'KLQPw=="/>'), + rendered_content ) def test_integrity_with_crossorigin_empty(self): @@ -255,6 +260,7 @@ def test_integrity_with_crossorigin_empty(self): request = self.factory.get('/') request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' result = view(request) + rendered_content = result.rendered_content self.assertIn(( '' - ), result.rendered_content) + 'crossorigin>' + ), rendered_content) self.assertIn(( ''), - result.rendered_content + 'crossorigin/>'), + rendered_content ) def test_integrity_with_crossorigin_anonymous(self): @@ -284,6 +290,7 @@ def test_integrity_with_crossorigin_anonymous(self): request = self.factory.get('/') request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' result = view(request) + rendered_content = result.rendered_content self.assertIn(( '' - ), result.rendered_content) + 'crossorigin="anonymous">' + ), rendered_content) self.assertIn(( ''), - result.rendered_content + 'crossorigin="anonymous"/>'), + rendered_content ) def test_integrity_with_crossorigin_use_credentials(self): @@ -313,6 +320,7 @@ def test_integrity_with_crossorigin_use_credentials(self): request = self.factory.get('/') request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' result = view(request) + rendered_content = result.rendered_content self.assertIn(( '' - ), result.rendered_content) + 'crossorigin="use-credentials">' + ), rendered_content) self.assertIn(( ''), - result.rendered_content + 'crossorigin="use-credentials"/>'), + rendered_content ) def test_integrity_missing_config(self): @@ -343,14 +351,15 @@ def test_integrity_missing_config(self): view = TemplateView.as_view(template_name='single.html') request = self.factory.get('/') result = view(request) + rendered_content = result.rendered_content self.assertIn(( - ''), result.rendered_content + ''), rendered_content ) self.assertIn(( - ''), - result.rendered_content + ''), + rendered_content ) # return removed key @@ -372,7 +381,7 @@ def test_append_extensions(self): result = view(request) self.assertIn(( - ''), result.rendered_content) def test_jinja2(self): @@ -395,12 +404,14 @@ def test_jinja2(self): with self.settings(**settings): request = self.factory.get('/') result = view(request) + rendered_content = result.rendered_content + self.assertIn(( ''), result.rendered_content) + '/main.css" rel="stylesheet"/>'), rendered_content) self.assertIn(( ''), result.rendered_content) + 'async charset="UTF-8">'), rendered_content) def test_reporting_errors(self): self.compile_bundles('webpack.config.error.js') @@ -420,6 +431,127 @@ def test_missing_bundle(self): 'Cannot resolve bundle {0}'.format(missing_bundle_name), str(e)) + def test_get_bundle_uses_same_assets_snapshot_when_iterated(self): + class ChangingAssetsLoader(WebpackLoader): + def __init__(self, name, config): + super().__init__(name, config) + self.load_assets_calls = 0 + self.assets_snapshots = [ + { + 'status': 'done', + 'chunks': {'main': ['old.js']}, + 'assets': {'old.js': {'name': 'old.js'}}, + }, + { + 'status': 'done', + 'chunks': {'main': ['new.js']}, + 'assets': {'new.js': {'name': 'new.js'}}, + }, + ] + + def load_assets(self): + snapshot_index = min( + self.load_assets_calls, len(self.assets_snapshots) - 1) + self.load_assets_calls += 1 + return self.assets_snapshots[snapshot_index] + + loader = ChangingAssetsLoader(DEFAULT_CONFIG, { + 'CACHE': False, + 'BUNDLE_DIR_NAME': 'django_webpack_loader_bundles/', + 'TIMEOUT': None, + 'POLL_INTERVAL': 0.1, + 'ignores': [], + 'INTEGRITY': False, + }) + + bundle = loader.get_bundle('main') + + self.assertEqual(list(bundle), [{ + 'name': 'old.js', + 'url': '/static/django_webpack_loader_bundles/old.js', + }]) + self.assertEqual(loader.load_assets_calls, 1) + + def test_cached_compile_status_is_reloaded_while_debug_polling(self): + class CompilingThenDoneLoader(WebpackLoader): + def __init__(self, name, config): + super().__init__(name, config) + self.load_assets_calls = 0 + self.assets_snapshots = [ + {'status': 'compile'}, + { + 'status': 'done', + 'chunks': {'main': ['main.js']}, + 'assets': {'main.js': {'name': 'main.js'}}, + }, + ] + + def load_assets(self): + snapshot_index = min( + self.load_assets_calls, len(self.assets_snapshots) - 1) + self.load_assets_calls += 1 + return self.assets_snapshots[snapshot_index] + + loader = CompilingThenDoneLoader('COMPILE_CACHE_DEBUG', { + 'CACHE': True, + 'BUNDLE_DIR_NAME': 'django_webpack_loader_bundles/', + 'TIMEOUT': 1, + 'POLL_INTERVAL': 0, + 'ignores': [], + 'INTEGRITY': False, + }) + + with self.settings(DEBUG=True): + bundle = loader.get_bundle('main') + + self.assertEqual(list(bundle), [{ + 'name': 'main.js', + 'url': '/static/django_webpack_loader_bundles/main.js', + }]) + self.assertEqual(loader.load_assets_calls, 2) + self.assertEqual(loader._assets[loader.name]['status'], 'done') + + def test_cached_compile_status_is_reloaded_after_bad_stats(self): + class CompilingThenDoneLoader(WebpackLoader): + def __init__(self, name, config): + super().__init__(name, config) + self.load_assets_calls = 0 + self.assets_snapshots = [ + {'status': 'compile'}, + { + 'status': 'done', + 'chunks': {'main': ['main.js']}, + 'assets': {'main.js': {'name': 'main.js'}}, + }, + ] + + def load_assets(self): + snapshot_index = min( + self.load_assets_calls, len(self.assets_snapshots) - 1) + self.load_assets_calls += 1 + return self.assets_snapshots[snapshot_index] + + loader = CompilingThenDoneLoader('COMPILE_CACHE_PRODUCTION', { + 'CACHE': True, + 'BUNDLE_DIR_NAME': 'django_webpack_loader_bundles/', + 'TIMEOUT': None, + 'POLL_INTERVAL': 0.1, + 'ignores': [], + 'INTEGRITY': False, + }) + + with self.settings(DEBUG=False): + with self.assertRaises(WebpackLoaderBadStatsError): + loader.get_bundle('main') + bundle = loader.get_bundle('main') + + self.assertEqual(list(bundle), [{ + 'name': 'main.js', + 'url': '/static/django_webpack_loader_bundles/main.js', + }]) + self.assertEqual(loader.load_assets_calls, 2) + self.assertEqual(loader._assets[loader.name]['status'], 'done') + def test_missing_stats_file(self): stats_file = settings.WEBPACK_LOADER[DEFAULT_CONFIG]['STATS_FILE'] if os.path.exists(stats_file): @@ -509,13 +641,13 @@ def test_emits_warning_on_no_request_in_djangoengine(self, warn_mock): """ self.compile_bundles('webpack.config.skipCommon.js') asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') # Shouldn't call any `warn()` here @@ -578,13 +710,13 @@ def test_emits_warning_on_no_request_in_jinja2engine(self, warn_mock): ] } asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') warning_call = MockCall( message=_WARNING_MESSAGE, category=RuntimeWarning) @@ -645,7 +777,7 @@ def test_get_files_emits_warning_on_no_request_in_djangoengine(self, warn_mock): '{% render_bundle "app1" %}' '{% get_files "app2" skip_common_chunks=True as app2_files %}' '{% for f in app2_files %}' - ' ' + ' ' '{% endfor %}'), ) # type: Template output = template.render(context=Context()) @@ -700,13 +832,13 @@ def _assert_common_chunks_duplicated_djangoengine(self, template): """ request = self.factory.get(path='/') asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') rendered_template = template.render( context=None, request=request) @@ -727,13 +859,13 @@ def _assert_common_chunks_not_duplicated_djangoengine(self, template): """ request = self.factory.get(path='/') asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') rendered_template = template.render( context=None, request=request) @@ -765,13 +897,13 @@ def _assert_common_chunks_duplicated_jinja2engine(self, view): ] } asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') with self.settings(**settings): @@ -808,13 +940,13 @@ def _assert_common_chunks_not_duplicated_jinja2engine(self, view): ] } asset_vendor = ( - '') asset_app1 = ( - '') asset_app2 = ( - '') with self.settings(**settings): @@ -933,16 +1065,68 @@ def test_skip_common_chunks_missing_config(self): # return removed key loader.config['SKIP_COMMON_CHUNKS'] = skip_common_chunks + def test_skip_common_chunks_get_files_then_get_files(self): + self.compile_bundles('webpack.config.skipCommon.js') + asset_vendor = '/static/django_webpack_loader_bundles/vendors.js' + asset_app2 = '/static/django_webpack_loader_bundles/app2.js' + + template = Template(template_string=( + '{% load render_bundle get_files from webpack_loader %}' + '{% get_files "app1" skip_common_chunks=True as app1_files %}' + '{% for f in app1_files %}{% endfor %}' + '{% get_files "app2" skip_common_chunks=True as app2_files %}' + '{% for f in app2_files %}{% endfor %}' + )) + request = self.factory.get(path='/') + output = template.render(context=Context({'request': request})) + + self.assertEqual(output.count(asset_vendor), 1) + self.assertIn(asset_app2, output) + + def test_skip_common_chunks_get_files_then_render_bundle(self): + self.compile_bundles('webpack.config.skipCommon.js') + asset_vendor = '/static/django_webpack_loader_bundles/vendors.js' + asset_app2 = '/static/django_webpack_loader_bundles/app2.js' + + template = Template(template_string=( + '{% load render_bundle get_files from webpack_loader %}' + '{% get_files "app1" skip_common_chunks=True as app1_files %}' + '{% for f in app1_files %}{% endfor %}' + '{% render_bundle "app2" skip_common_chunks=True %}' + )) + request = self.factory.get(path='/') + output = template.render(context=Context({'request': request})) + + self.assertEqual(output.count(asset_vendor), 1) + self.assertIn(asset_app2, output) + + def test_skip_common_chunks_render_bundle_then_get_files(self): + self.compile_bundles('webpack.config.skipCommon.js') + asset_vendor = '/static/django_webpack_loader_bundles/vendors.js' + asset_app2 = '/static/django_webpack_loader_bundles/app2.js' + + template = Template(template_string=( + '{% load render_bundle get_files from webpack_loader %}' + '{% render_bundle "app1" skip_common_chunks=True %}' + '{% get_files "app2" skip_common_chunks=True as app2_files %}' + '{% for f in app2_files %}{% endfor %}' + )) + request = self.factory.get(path='/') + output = template.render(context=Context({'request': request})) + + self.assertEqual(output.count(asset_vendor), 1) + self.assertIn(asset_app2, output) + def test_get_as_tags_direct_usage(self): self.compile_bundles('webpack.config.skipCommon.js') asset_vendor = ( - '') asset_app1 = ( - '') + '') asset_app2 = ( - '') tags = get_as_tags('app1') @@ -1002,6 +1186,22 @@ def test_get_url_to_tag_dict_with_nonce_disabled(self): tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request_with_nonce) self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/main.js']) + def test_get_url_to_tag_dict_js_preload_includes_integrity_and_nonce(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CSP_NONCE': True, 'CACHE': False}): + request = self.factory.get('/') + request.csp_nonce = 'test-nonce' + + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request, is_preload=True) + tag = tag_dict['http://custom-static-host.com/main.js'] + + self.assertIn('rel="preload"', tag) + self.assertIn('as="script"', tag) + self.assertIn('integrity=', tag) + self.assertIn('nonce="test-nonce"', tag) + def test_get_url_to_tag_dict_with_different_extensions(self): """Test the get_as_url_to_tag_dict function with different file extensions.""" diff --git a/tests_webpack5/app/tests/test_webpack.py b/tests_webpack5/app/tests/test_webpack.py index 04bfb1f3..b7645ccf 100644 --- a/tests_webpack5/app/tests/test_webpack.py +++ b/tests_webpack5/app/tests/test_webpack.py @@ -46,7 +46,7 @@ def test_integrity(self): 'integrity="sha256-tq+bx/AOKBO9HvojLMT+nwLvdzX5q9s5hGI8sJr' 'V+6Q= sha384-MQ3aER73Wrl5JjMLWVotKhBZk4e9/67+xrO8/qqACm7a' '695zI9sgQKa6bC54TMvb sha512-dKT17sF4HfpJC+UMIjQch07waKpAt' - 'Tvv9GM2s/eGomGDbCKpKHGp29+6SRrQSrT2+6IF3YGu3BaoQAoAS4opOQ==" ' + 'Tvv9GM2s/eGomGDbCKpKHGp29+6SRrQSrT2+6IF3YGu3BaoQAoAS4opOQ=="' '>'), result.rendered_content) def test_integrity_with_crossorigin_empty(self): @@ -65,7 +65,7 @@ def test_integrity_with_crossorigin_empty(self): 'V+6Q= sha384-MQ3aER73Wrl5JjMLWVotKhBZk4e9/67+xrO8/qqACm7a' '695zI9sgQKa6bC54TMvb sha512-dKT17sF4HfpJC+UMIjQch07waKpAt' 'Tvv9GM2s/eGomGDbCKpKHGp29+6SRrQSrT2+6IF3YGu3BaoQAoAS4opOQ==" ' - 'crossorigin >' + 'crossorigin>' ), result.rendered_content) def test_integrity_with_crossorigin_anonymous(self): @@ -84,7 +84,7 @@ def test_integrity_with_crossorigin_anonymous(self): 'V+6Q= sha384-MQ3aER73Wrl5JjMLWVotKhBZk4e9/67+xrO8/qqACm7a' '695zI9sgQKa6bC54TMvb sha512-dKT17sF4HfpJC+UMIjQch07waKpAt' 'Tvv9GM2s/eGomGDbCKpKHGp29+6SRrQSrT2+6IF3YGu3BaoQAoAS4opOQ==" ' - 'crossorigin="anonymous" >' + 'crossorigin="anonymous">' ), result.rendered_content) def test_integrity_with_crossorigin_use_credentials(self): @@ -103,7 +103,7 @@ def test_integrity_with_crossorigin_use_credentials(self): 'V+6Q= sha384-MQ3aER73Wrl5JjMLWVotKhBZk4e9/67+xrO8/qqACm7a' '695zI9sgQKa6bC54TMvb sha512-dKT17sF4HfpJC+UMIjQch07waKpAt' 'Tvv9GM2s/eGomGDbCKpKHGp29+6SRrQSrT2+6IF3YGu3BaoQAoAS4opOQ==" ' - 'crossorigin="use-credentials" >' + 'crossorigin="use-credentials">' ), result.rendered_content) def test_get_url_to_tag_dict_with_nonce(self): @@ -155,3 +155,19 @@ def test_get_url_to_tag_dict_with_nonce_disabled(self): # Test with CSP_NONCE disabled - should not have nonce tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=request_with_nonce) self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + def test_get_url_to_tag_dict_js_preload_includes_integrity_and_nonce(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CSP_NONCE': True, 'CACHE': False}): + request = self.factory.get('/') + request.csp_nonce = 'test-nonce' + + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=request, is_preload=True) + tag = tag_dict['http://custom-static-host.com/resources.js'] + + self.assertIn('rel="preload"', tag) + self.assertIn('as="script"', tag) + self.assertIn('integrity=', tag) + self.assertIn('nonce="test-nonce"', tag) diff --git a/webpack_loader/loaders.py b/webpack_loader/loaders.py index 268d52d0..27a74c88 100644 --- a/webpack_loader/loaders.py +++ b/webpack_loader/loaders.py @@ -66,9 +66,12 @@ def load_assets(self): def get_assets(self): if self.config["CACHE"]: - if self.name not in self._assets: - self._assets[self.name] = self.load_assets() - return self._assets[self.name] + assets = self._assets.get(self.name) + if not assets or assets.get("status") == "compile": + assets = self.load_assets() + if assets.get("status") != "compile": + self._assets[self.name] = assets + return assets return self.load_assets() def get_asset_by_source_filename(self, name): @@ -79,7 +82,7 @@ def _add_crossorigin( self, request: Optional[HttpRequest], chunk: Dict[str, str], integrity: str, attrs_l: str) -> str: 'Return an added `crossorigin` attribute if necessary.' - def_value = f' integrity="{integrity}" ' + def_value = f'integrity="{integrity}"' if not request: message = _CROSSORIGIN_NO_REQUEST.format(chunk_name=chunk['name']) warn(message=message, category=RuntimeWarning) @@ -97,15 +100,15 @@ def _add_crossorigin( return def_value cfgval: str = self.config.get('CROSSORIGIN') if cfgval == '': - return f'{def_value}crossorigin ' - return f'{def_value}crossorigin="{cfgval}" ' + return f'{def_value} crossorigin' + return f'{def_value} crossorigin="{cfgval}"' def get_integrity_attr( self, chunk: Dict[str, str], request: Optional[HttpRequest], - attrs_l: str) -> str: + attrs_l: str) -> Optional[str]: if not self.config.get('INTEGRITY'): # Crossorigin only necessary when integrity is used - return ' ' + return None integrity = chunk.get('integrity') if not integrity: @@ -122,10 +125,12 @@ def get_integrity_attr( attrs_l=attrs_l, ) - def get_nonce_attr(self, chunk: Dict[str, str], request: Optional[HttpRequest], attrs: str) -> str: + def get_nonce_attr( + self, chunk: Dict[str, str], request: Optional[HttpRequest], + attrs: str) -> Optional[str]: 'Return an added nonce for CSP when available.' if not self.config.get('CSP_NONCE'): - return '' + return None if request is None: message = _NONCE_NO_REQUEST.format(chunk_name=chunk['name']) warn(message=message, category=RuntimeWarning) @@ -137,7 +142,7 @@ def get_nonce_attr(self, chunk: Dict[str, str], request: Optional[HttpRequest], return '' if 'nonce=' in attrs.lower(): return '' - return f'nonce="{nonce}" ' + return f'nonce="{nonce}"' def filter_chunks(self, chunks): filtered_chunks = [] @@ -149,20 +154,21 @@ def filter_chunks(self, chunks): return filtered_chunks - def map_chunk_files_to_url(self, chunks): - assets = self.get_assets() + def map_chunk_files_to_url(self, chunks, assets=None): + assets = assets or self.get_assets() files = assets["assets"] add_integrity = self.config.get("INTEGRITY") for chunk in chunks: - url = self.get_chunk_url(files[chunk]) + asset = files[chunk] + url = self.get_chunk_url(asset) if add_integrity: yield { "name": chunk, "url": url, - "integrity": files[chunk].get("integrity"), + "integrity": asset.get("integrity"), } else: yield {"name": chunk, "url": url} @@ -219,7 +225,7 @@ def get_bundle(self, bundle_name): "Cannot resolve asset {0}.".format(chunk) ) - return self.map_chunk_files_to_url(filtered_chunks) + return self.map_chunk_files_to_url(filtered_chunks, assets=assets) elif assets.get("status") == "error": if "file" not in assets: diff --git a/webpack_loader/templatetags/webpack_loader.py b/webpack_loader/templatetags/webpack_loader.py index 5b838a7a..29dd1ffa 100644 --- a/webpack_loader/templatetags/webpack_loader.py +++ b/webpack_loader/templatetags/webpack_loader.py @@ -82,8 +82,10 @@ def get_files( return result used_urls = getattr(request, '_webpack_loader_used_urls', None) - if not used_urls: + if used_urls is None: used_urls = set() + setattr(request, '_webpack_loader_used_urls', used_urls) if skip_common_chunks: result = [chunk for chunk in result if chunk['url'] not in used_urls] + used_urls.update(chunk['url'] for chunk in result) return result diff --git a/webpack_loader/utils.py b/webpack_loader/utils.py index e789f70b..9f833834 100644 --- a/webpack_loader/utils.py +++ b/webpack_loader/utils.py @@ -79,29 +79,23 @@ def get_as_url_to_tag_dict( attrs_l = attrs.lower() for chunk in bundle: + url = ''.join([chunk['url'], suffix]) + integrity = loader.get_integrity_attr(chunk, request, attrs_l) + nonce = loader.get_nonce_attr(chunk, request, attrs_l) + extra = ' '.join(filter(bool, [integrity, nonce, attrs.strip()])) + attrs_str = f' {extra}' if extra else '' + if chunk['name'].endswith(('.js', '.js.gz')): if is_preload: result[chunk['url']] = ( - '' - ).format(''.join([chunk['url'], suffix]), attrs) - else: - result[chunk['url']] = ( - '' - ).format( - ''.join([chunk['url'], suffix]), - attrs, - loader.get_integrity_attr(chunk, request, attrs_l), - loader.get_nonce_attr(chunk, request, attrs_l), + f'' ) + else: + result[chunk['url']] = f'' elif chunk['name'].endswith(('.css', '.css.gz')): + rel = '"stylesheet"' if not is_preload else '"preload" as="style"' result[chunk['url']] = ( - '' - ).format( - ''.join([chunk['url'], suffix]), - attrs, - '"stylesheet"' if not is_preload else '"preload" as="style"', - loader.get_integrity_attr(chunk, request, attrs_l), - loader.get_nonce_attr(chunk, request, attrs_l), + f'' ) return result