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