diff --git a/.circleci/config.yml b/.circleci/config.yml index 38428843..85aaecfe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,8 +6,8 @@ workflows: - base-test: matrix: parameters: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - django-version: ["3.2", "4.2", "5.0", "5.1"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + django-version: ["4.2", "5.0", "5.1", "5.2"] exclude: - python-version: "3.8" django-version: "5.0" @@ -17,6 +17,10 @@ workflows: django-version: "5.1" - python-version: "3.9" django-version: "5.1" + - python-version: "3.8" + django-version: "5.2" + - python-version: "3.9" + django-version: "5.2" - coverall: requires: - base-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca50111..29bdb807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ For more general information, view the [readme](README.md). Releases are added to the [github release page](https://github.com/ezhome/django-webpack-loader/releases). +## [3.2.0] -- 2025-05-19 + +- Automatically add `crossorigin` attributes to tags with `integrity` attributes when necessary +- Use `request.csp_nonce` from [django-csp](https://github.com/mozilla/django-csp) if available and configured +- Remove support for Django 3.x (LTS is EOL) + ## [3.1.1] -- 2024-08-30 - Add support for Django 5.1 diff --git a/README.md b/README.md index 3f9bf559..7131c80e 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,11 @@ WEBPACK_LOADER = { - `TIMEOUT` is the number of seconds webpack_loader should wait for Webpack to finish compiling before raising an exception. `0`, `None` or leaving the value out of settings disables timeouts -- `INTEGRITY` is flag enabling [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) on rendered `'), result.rendered_content) self.assertIn(( - ''), + ''), + result.rendered_content + ) + + def test_integrity_with_crossorigin_empty(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': '', 'CACHE': False}): + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + self.assertIn(( + ''), + result.rendered_content + ) + + def test_integrity_with_crossorigin_anonymous(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': 'anonymous', 'CACHE': False}): + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + self.assertIn(( + ''), + result.rendered_content + ) + + def test_integrity_with_crossorigin_use_credentials(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': 'use-credentials', 'CACHE': False}): + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + self.assertIn(( + ''), result.rendered_content ) @@ -244,11 +340,11 @@ def test_integrity_missing_config(self): result = view(request) self.assertIn(( - ''), result.rendered_content ) self.assertIn(( - ''), + ''), result.rendered_content ) @@ -850,3 +946,76 @@ def test_get_as_tags_direct_usage(self): self.assertEqual(tags[0], asset_vendor) self.assertEqual(tags[1], asset_app1) self.assertEqual(tags[2], asset_app2) + + def test_get_url_to_tag_dict_with_nonce(self): + """Test the get_as_url_to_tag_dict function with nonce attribute handling.""" + + self.compile_bundles('webpack.config.simple.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {"CSP_NONCE": True, 'CACHE': False}): + # Create a request with csp_nonce + request = self.factory.get('/') + request.csp_nonce = "test-nonce-123" + + # Get tag dict with nonce enabled + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request) + # Verify nonce is in the tag + self.assertIn('nonce="test-nonce-123"', tag_dict['/static/django_webpack_loader_bundles/main.js']) + + # Test with existing nonce in attrs - should not duplicate + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='nonce="existing-nonce"', request=request) + self.assertIn('nonce="existing-nonce"', tag_dict['/static/django_webpack_loader_bundles/main.js']) + self.assertNotIn('nonce="test-nonce-123"', tag_dict['/static/django_webpack_loader_bundles/main.js']) + + # Test without request - should not have nonce + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=None) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/main.js']) + + # Test with request but no csp_nonce attribute - should not have nonce + request_without_nonce = self.factory.get('/') + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request_without_nonce) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/main.js']) + + def test_get_url_to_tag_dict_with_nonce_disabled(self): + self.compile_bundles('webpack.config.simple.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {"CSP_NONCE": False, 'CACHE': False}): + # Create a request without csp_nonce + request = self.factory.get('/') + + # should not have nonce + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/main.js']) + + # Create a request with csp_nonce + request_with_nonce = self.factory.get('/') + request_with_nonce.csp_nonce = "test-nonce-123" + + # Test with CSP_NONCE disabled - should not have nonce + 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_with_different_extensions(self): + """Test the get_as_url_to_tag_dict function with different file extensions.""" + + self.compile_bundles('webpack.config.simple.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {"CSP_NONCE": True, 'CACHE': False}): + # Create a request with csp_nonce + request = self.factory.get('/') + request.csp_nonce = "test-nonce-123" + + # JavaScript file + tag_dict = get_as_url_to_tag_dict('main', extension='js', attrs='', request=request) + self.assertIn(''), result.rendered_content) + + def test_integrity_with_crossorigin_empty(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': '', 'CACHE': False}): + view = TemplateView.as_view(template_name='home.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + + def test_integrity_with_crossorigin_anonymous(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': 'anonymous', 'CACHE': False}): + view = TemplateView.as_view(template_name='home.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + + def test_integrity_with_crossorigin_use_credentials(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True, 'CROSSORIGIN': 'use-credentials', 'CACHE': False}): + view = TemplateView.as_view(template_name='home.html') + request = self.factory.get('/') + request.META['HTTP_HOST'] = 'crossorigin-custom-static-host.com' + result = view(request) + + self.assertIn(( + '' + ), result.rendered_content) + + def test_get_url_to_tag_dict_with_nonce(self): + """Test the get_as_url_to_tag_dict function with nonce attribute handling.""" + + self.compile_bundles('webpack.config.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {"CSP_NONCE": True, 'CACHE': False}): + # Create a request with csp_nonce + request = self.factory.get('/') + request.csp_nonce = "test-nonce-123" + + # Get tag dict with nonce enabled + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=request) + # Verify nonce is in the tag + self.assertIn('nonce="test-nonce-123"', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + # Test with existing nonce in attrs - should not duplicate + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='nonce="existing-nonce"', request=request) + self.assertIn('nonce="existing-nonce"', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + self.assertNotIn('nonce="test-nonce-123"', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + # Test without request - should not have nonce + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=None) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + # Test with request but no csp_nonce attribute - should not have nonce + request_without_nonce = self.factory.get('/') + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=request_without_nonce) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + def test_get_url_to_tag_dict_with_nonce_disabled(self): + self.compile_bundles('webpack.config.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {"CSP_NONCE": False, 'CACHE': False}): + # Create a request without csp_nonce + request = self.factory.get('/') + + # should not have nonce + tag_dict = get_as_url_to_tag_dict('resources', extension='js', attrs='', request=request) + self.assertNotIn('nonce=', tag_dict['/static/django_webpack_loader_bundles/resources.js']) + + # Create a request with csp_nonce + request_with_nonce = self.factory.get('/') + request_with_nonce.csp_nonce = "test-nonce-123" + + # 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']) diff --git a/tests_webpack5/package-lock.json b/tests_webpack5/package-lock.json index cdb44430..e380978e 100644 --- a/tests_webpack5/package-lock.json +++ b/tests_webpack5/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "webpack": "^5.92.0", - "webpack-bundle-tracker": "3.1.1", + "webpack-bundle-tracker": "3.2.0", "webpack-cli": "^5.1.4" } }, @@ -787,12 +787,6 @@ "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, "node_modules/lodash.topairs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", @@ -1276,16 +1270,15 @@ } }, "node_modules/webpack-bundle-tracker": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-3.1.1.tgz", - "integrity": "sha512-4gBt5EPKZyjy48Djr+75KTfLH+ikqDklgQD+padwuTc9y+ULHl9B4zqnZkV6cTdH3R2KzQYUZDQzpGUu+k6b/A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-3.2.0.tgz", + "integrity": "sha512-/CV+Mre7jC+lSTjF7nYv36FpdwIQR15URKGHpLmdgGxxdYFQ858MV13J0qqR7Rn3wke3O41yX0tNntPv3uo1QA==", "dev": true, "dependencies": { "lodash.assign": "^4.2.0", "lodash.defaults": "^4.2.0", "lodash.foreach": "^4.5.0", "lodash.frompairs": "^4.0.1", - "lodash.get": "^4.4.2", "lodash.topairs": "^4.3.0" } }, diff --git a/tests_webpack5/package.json b/tests_webpack5/package.json index 70a0e517..7cf0d8f7 100644 --- a/tests_webpack5/package.json +++ b/tests_webpack5/package.json @@ -6,7 +6,7 @@ "license": "MIT", "devDependencies": { "webpack": "^5.92.0", - "webpack-bundle-tracker": "3.1.1", + "webpack-bundle-tracker": "3.2.0", "webpack-cli": "^5.1.4" } } diff --git a/tests_webpack5/webpack.config.integrity.js b/tests_webpack5/webpack.config.integrity.js new file mode 100644 index 00000000..c4af8761 --- /dev/null +++ b/tests_webpack5/webpack.config.integrity.js @@ -0,0 +1,27 @@ +var path = require("path"); +var BundleTracker = require('webpack-bundle-tracker'); + +module.exports = { + entry: { + resources: './assets/js/resources' + }, + + output: { + assetModuleFilename: 'assets/[name]-[contenthash][ext]', + path: path.resolve('./assets/django_webpack_loader_bundles/'), + publicPath: 'http://custom-static-host.com/', + }, + + module: { + rules: [{ test: /\.txt$/, type: 'asset/resource' }] + }, + + plugins: [ + new BundleTracker({path: __dirname, integrity: true}) + ], + + resolve: { + extensions: ['.js', '.jsx'] + }, +} + diff --git a/webpack_loader/__init__.py b/webpack_loader/__init__.py index 7ad82065..30031b94 100644 --- a/webpack_loader/__init__.py +++ b/webpack_loader/__init__.py @@ -1,5 +1,5 @@ __author__ = "Vinta Software" -__version__ = "3.1.1" +__version__ = "3.2.0" import django diff --git a/webpack_loader/config.py b/webpack_loader/config.py index 7045224d..e8263e75 100644 --- a/webpack_loader/config.py +++ b/webpack_loader/config.py @@ -16,9 +16,15 @@ 'IGNORE': [r'.+\.hot-update.js', r'.+\.map'], 'LOADER_CLASS': 'webpack_loader.loaders.WebpackLoader', 'INTEGRITY': False, + # See https://shubhamjain.co/2018/09/08/subresource-integrity-crossorigin/ + # See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin + # type is Literal['anonymous', 'use-credentials', ''] + 'CROSSORIGIN': '', # Whenever the global setting for SKIP_COMMON_CHUNKS is changed, please # update the fallback value in get_skip_common_chunks (utils.py). 'SKIP_COMMON_CHUNKS': False, + # Use nonces from django-csp when available + 'CSP_NONCE': False } } diff --git a/webpack_loader/loaders.py b/webpack_loader/loaders.py index 9e6c52aa..2fde9310 100644 --- a/webpack_loader/loaders.py +++ b/webpack_loader/loaders.py @@ -1,18 +1,47 @@ import json -import time import os +import time +from functools import lru_cache from io import open +from typing import Dict, Optional +from urllib.parse import urlparse +from warnings import warn from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage +from django.http.request import HttpRequest from .exceptions import ( + WebpackBundleLookupError, WebpackError, WebpackLoaderBadStatsError, WebpackLoaderTimeoutError, - WebpackBundleLookupError, ) +_CROSSORIGIN_NO_REQUEST = ( + 'The crossorigin attribute might be necessary but you did not pass a ' + 'request object. django_webpack_loader needs a request object to be able ' + 'to know when to emit the crossorigin attribute on link and script tags. ' + 'Chunk name: {chunk_name}') +_CROSSORIGIN_NO_HOST = ( + 'You have passed the request object but it does not have a "HTTP_HOST", ' + 'thus django_webpack_loader can\'t know if the crossorigin header will ' + 'be necessary or not. Chunk name: {chunk_name}') +_NONCE_NO_REQUEST = ( + 'You have enabled the adding of nonce attributes to generated tags via ' + 'django_webpack_loader, but haven\'t passed a request. ' + 'Chunk name: {chunk_name}') +_NONCE_NO_CSPNONCE = ( + 'django_webpack_loader can\'t generate a nonce tag for a bundle, ' + 'because the passed request doesn\'t contain a "csp_nonce". ' + 'Chunk name: {chunk_name}') + + +@lru_cache(maxsize=100) +def _get_netloc(url: str) -> str: + 'Return a cached netloc (host:port) for the passed `url`.' + return urlparse(url=url).netloc + class WebpackLoader: _assets = {} @@ -42,19 +71,69 @@ def get_asset_by_source_filename(self, name): files = self.get_assets()["assets"].values() return next((x for x in files if x.get("sourceFilename") == name), None) - def get_integrity_attr(self, chunk): - if not self.config.get("INTEGRITY"): - return " " - - integrity = chunk.get("integrity") + 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}" ' + if not request: + message = _CROSSORIGIN_NO_REQUEST.format(chunk_name=chunk['name']) + warn(message=message, category=RuntimeWarning) + return def_value + if 'crossorigin' in attrs_l: + return def_value + host: Optional[str] = request.META.get('HTTP_HOST') + if not host: + message = _CROSSORIGIN_NO_HOST.format(chunk_name=chunk['name']) + warn(message=message, category=RuntimeWarning) + return def_value + netloc = _get_netloc(url=chunk['url']) + if netloc == '' or netloc == host: + # Crossorigin not necessary + return def_value + cfgval: str = self.config.get('CROSSORIGIN') + if 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: + if not self.config.get('INTEGRITY'): + # Crossorigin only necessary when integrity is used + return ' ' + + integrity = chunk.get('integrity') if not integrity: raise WebpackLoaderBadStatsError( - "The stats file does not contain valid data: INTEGRITY is set to True, " - 'but chunk does not contain "integrity" key. Maybe you forgot to add ' - "integrity: true in your BundleTracker configuration?" + 'The stats file does not contain valid data: INTEGRITY is set ' + 'to True, but chunk does not contain "integrity" key. Maybe ' + 'you forgot to add integrity: true in your ' + 'BundleTrackerPlugin configuration?' ) + return self._add_crossorigin( + request=request, + chunk=chunk, + integrity=integrity, + attrs_l=attrs_l, + ) - return ' integrity="{}" '.format(integrity.partition(" ")[0]) + def get_nonce_attr(self, chunk: Dict[str, str], request: Optional[HttpRequest], attrs: str) -> str: + 'Return an added nonce for CSP when available.' + if not self.config.get('CSP_NONCE'): + return '' + if request is None: + message = _NONCE_NO_REQUEST.format(chunk_name=chunk['name']) + warn(message=message, category=RuntimeWarning) + return '' + nonce = getattr(request, 'csp_nonce', None) + if nonce is None: + message = _NONCE_NO_CSPNONCE.format(chunk_name=chunk['name']) + warn(message=message, category=RuntimeWarning) + return '' + if 'nonce=' in attrs.lower(): + return '' + return f'nonce="{nonce}" ' def filter_chunks(self, chunks): filtered_chunks = [] diff --git a/webpack_loader/templatetags/webpack_loader.py b/webpack_loader/templatetags/webpack_loader.py index e70c6dd7..5b838a7a 100644 --- a/webpack_loader/templatetags/webpack_loader.py +++ b/webpack_loader/templatetags/webpack_loader.py @@ -1,5 +1,7 @@ +from typing import Optional from warnings import warn +from django.http.request import HttpRequest from django.template import Library from django.utils.safestring import mark_safe @@ -20,33 +22,36 @@ def render_bundle( if skip_common_chunks is None: skip_common_chunks = utils.get_skip_common_chunks(config) - url_to_tag_dict = utils.get_as_url_to_tag_dict( - bundle_name, extension=extension, config=config, suffix=suffix, - attrs=attrs, is_preload=is_preload) + request: Optional[HttpRequest] = context.get('request') + tags = utils.get_as_url_to_tag_dict( + bundle_name, request=request, extension=extension, config=config, + suffix=suffix, attrs=attrs, is_preload=is_preload) - request = context.get('request') if request is None: if skip_common_chunks: warn(message=_WARNING_MESSAGE, category=RuntimeWarning) - return mark_safe('\n'.join(url_to_tag_dict.values())) + return mark_safe('\n'.join(tags.values())) used_urls = getattr(request, '_webpack_loader_used_urls', None) - if not used_urls: - used_urls = request._webpack_loader_used_urls = set() + if used_urls is None: + used_urls = set() + setattr(request, '_webpack_loader_used_urls', used_urls) if skip_common_chunks: - url_to_tag_dict = {url: tag for url, tag in url_to_tag_dict.items() if url not in used_urls} - used_urls.update(url_to_tag_dict.keys()) - return mark_safe('\n'.join(url_to_tag_dict.values())) + tags = {url: tag for url, tag in tags.items() if url not in used_urls} + used_urls.update(tags) + return mark_safe('\n'.join(tags.values())) @register.simple_tag def webpack_static(asset_name, config='DEFAULT'): return utils.get_static(asset_name, config=config) + @register.simple_tag def webpack_asset(asset_name, config='DEFAULT'): return utils.get_asset(asset_name, config=config) + @register.simple_tag(takes_context=True) def get_files( context, bundle_name, extension=None, config='DEFAULT', diff --git a/webpack_loader/utils.py b/webpack_loader/utils.py index 25423c02..e789f70b 100644 --- a/webpack_loader/utils.py +++ b/webpack_loader/utils.py @@ -1,9 +1,12 @@ -from collections import OrderedDict +from functools import lru_cache from importlib import import_module +from typing import Optional, OrderedDict + from django.conf import settings -from .config import load_config +from django.http.request import HttpRequest -_loaders = {} +from .config import load_config +from .loaders import WebpackLoader def import_string(dotted_path): @@ -21,12 +24,11 @@ def import_string(dotted_path): raise ImportError('%s doesn\'t look like a valid module path' % dotted_path) -def get_loader(config_name): - if config_name not in _loaders: - config = load_config(config_name) - loader_class = import_string(config['LOADER_CLASS']) - _loaders[config_name] = loader_class(config_name, config) - return _loaders[config_name] +@lru_cache(maxsize=None) +def get_loader(config_name) -> WebpackLoader: + config = load_config(config_name) + loader_class = import_string(config['LOADER_CLASS']) + return loader_class(config_name, config) def get_skip_common_chunks(config_name): @@ -57,7 +59,10 @@ def get_files(bundle_name, extension=None, config='DEFAULT'): return list(_get_bundle(loader, bundle_name, extension)) -def get_as_url_to_tag_dict(bundle_name, extension=None, config='DEFAULT', suffix='', attrs='', is_preload=False): +def get_as_url_to_tag_dict( + bundle_name, request: Optional[HttpRequest] = None, extension=None, + config='DEFAULT', suffix='', attrs='', is_preload=False +) -> OrderedDict[str, str]: ''' Get a dict of URLs to formatted ' + '' ).format( ''.join([chunk['url'], suffix]), attrs, - loader.get_integrity_attr(chunk), + loader.get_integrity_attr(chunk, request, attrs_l), + loader.get_nonce_attr(chunk, request, attrs_l), ) elif chunk['name'].endswith(('.css', '.css.gz')): result[chunk['url']] = ( - '' + '' ).format( ''.join([chunk['url'], suffix]), attrs, '"stylesheet"' if not is_preload else '"preload" as="style"', - loader.get_integrity_attr(chunk), + loader.get_integrity_attr(chunk, request, attrs_l), + loader.get_nonce_attr(chunk, request, attrs_l), ) return result -def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs='', is_preload=False): +def get_as_tags( + bundle_name, request=None, extension=None, config='DEFAULT', suffix='', + attrs='', is_preload=False): ''' Get a list of formatted