diff --git a/.circleci/config.yml b/.circleci/config.yml
index 30a7f7db..85aaecfe 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -6,13 +6,21 @@ 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"]
+ 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"
- python-version: "3.9"
django-version: "5.0"
+ - python-version: "3.8"
+ 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/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..6828d463
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+updates:
+ # Ignore all updates to the `tests` directory (old webpack 4 tests)
+ - package-ecosystem: "npm"
+ directory: "/tests"
+ schedule:
+ interval: "weekly"
+ ignore:
+ - dependency-name: "*"
+
+ - package-ecosystem: "npm"
+ directory: "/tests_webpack"
+ schedule:
+ interval: "weekly"
+
+ - package-ecosystem: "npm"
+ directory: "/examples"
+ schedule:
+ interval: "weekly"
diff --git a/.gitignore b/.gitignore
index f3ed53ec..4a4a4ac2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,14 +65,26 @@ target/
# Editors and IDEs
.ropeproject/
+# PyCharm IDE
+.idea
+.vscode
+
+# pyenv
.python-version
+# General
+.direnv
+.envrc
+.tool-versions
+
+# Project-specific
examples/**/ve/
examples/**/venv/
examples/**/node_modules/
examples/**/assets/bundles/
examples/**/assets/webpack_bundles/
examples/**/webpack-stats.json
+examples/**/package-lock.json
tests/ve/
tests/ve3/
@@ -84,12 +96,4 @@ tests/assets/webpack_bundles/
tests/assets/django_webpack_loader_bundles/
tests/webpack-stats.json
tests/webpack-stats-app2.json
-
-# PyCharm IDE
-.idea
-.vscode
-
-# General
-.direnv
-.envrc
-.tool-versions
\ No newline at end of file
+tests/webpack-stats-getFiles.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 006a795a..532ee9c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,28 @@ 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.1] -- 2025-06-16
+
+- Update supported versions on Trove classifiers
+
+## [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
+
+## [3.1.0] -- 2024-04-04
+
+Support `webpack_asset` template tag to render transformed assets URL: `{% webpack_asset 'path/to/original/file' %} == "/static/assets/resource-3c9e4020d3e3c7a09c68.txt"`
+
+## [3.0.1] -- 2024-01-16
+
+Added `skip_common_chunks` option to the `get_files()` template tag.
+
## [3.0.0] -- 2023-12-19
- Fix support for `publicPath: auto` in Webpack config, check updated examples at https://github.com/django-webpack/django-webpack-loader/tree/master/examples
diff --git a/Makefile b/Makefile
index 33752863..9978290f 100644
--- a/Makefile
+++ b/Makefile
@@ -36,6 +36,7 @@ test:
@$(activate_venv_if_not_active)
@echo "Running tests..."
@cd tests; coverage run --source=webpack_loader manage.py test
+ @cd tests_webpack5; coverage run --source=webpack_loader manage.py test
publish: build
@echo "Publishing to $(REPOSITORY)..."
diff --git a/README.md b/README.md
index 216d9343..7131c80e 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ pip install django-webpack-loader
## Configuration
-For kick-starting a full example project with opinionated development and production settings, you can check the [`django-react-boilerplate`](https://github.com/vintasoftware/django-react-boilerplate/blob/main/package.json). For a more flexible configuration, keep reading.
+For kick-starting a full example project with opinionated development and production settings, you can check the [`django-react-boilerplate`](https://github.com/vintasoftware/django-react-boilerplate/). For a more flexible configuration, keep reading.
### Configuring `webpack-bundle-tracker`
@@ -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
)
@@ -241,11 +340,11 @@ def test_integrity_missing_config(self):
result = view(request)
self.assertIn((
- ''), result.rendered_content
)
self.assertIn((
- ''),
+ ''),
result.rendered_content
)
@@ -373,17 +472,23 @@ def test_request_blocking(self):
t2 = Thread(
target=self.compile_bundles,
args=('webpack.config.app2.js', wait_for))
+ t3 = Thread(
+ target=self.compile_bundles,
+ args=('webpack.config.getFiles.js', wait_for))
t.start()
t2.start()
+ t3.start()
result.rendered_content
elapsed = time.time() - then
t.join()
t2.join()
+ t3.join()
self.assertTrue(elapsed >= wait_for)
with self.settings(DEBUG=False):
self.compile_bundles('webpack.config.simple.js')
self.compile_bundles('webpack.config.app2.js')
+ self.compile_bundles('webpack.config.getFiles.js')
then = time.time()
request = self.factory.get('/')
result = view(request)
@@ -391,10 +496,8 @@ def test_request_blocking(self):
elapsed = time.time() - then
self.assertTrue(elapsed < wait_for)
- @patch(
- target='webpack_loader.templatetags.webpack_loader.warn',
- new=_warn_mock)
- def test_emits_warning_on_no_request_in_djangoengine(self):
+ @patch(target='webpack_loader.templatetags.webpack_loader.warn')
+ def test_emits_warning_on_no_request_in_djangoengine(self, warn_mock):
"""
Should emit warnings on having no request in context (django
template).
@@ -416,7 +519,7 @@ def test_emits_warning_on_no_request_in_djangoengine(self):
r'{% render_bundle "app1" %}'
r'{% render_bundle "app2" %}')) # type: Template
output = dups_template.render(context=Context())
- _warn_mock.assert_not_called()
+ warn_mock.assert_not_called()
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 2)
@@ -431,11 +534,11 @@ def test_emits_warning_on_no_request_in_djangoengine(self):
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 2)
- _warn_mock.assert_called_once_with(
+ warn_mock.assert_called_once_with(
message=_WARNING_MESSAGE, category=RuntimeWarning)
# Should NOT call `warn()` here
- _warn_mock.reset_mock()
+ warn_mock.reset_mock()
nodups_template = Template(template_string=(
r'{% load render_bundle from webpack_loader %}'
r'{% render_bundle "app1" %}'
@@ -443,19 +546,17 @@ def test_emits_warning_on_no_request_in_djangoengine(self):
) # type: Template
request = self.factory.get(path='/')
output = nodups_template.render(context=Context({'request': request}))
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 1)
- _warn_mock.assert_not_called()
- _warn_mock.reset_mock()
+ warn_mock.assert_not_called()
+ warn_mock.reset_mock()
- @patch(
- target='webpack_loader.templatetags.webpack_loader.warn',
- new=_warn_mock)
- def test_emits_warning_on_no_request_in_jinja2engine(self):
+ @patch(target='webpack_loader.templatetags.webpack_loader.warn')
+ def test_emits_warning_on_no_request_in_jinja2engine(self, warn_mock):
'Should emit warnings on having no request in context (Jinja2).'
self.compile_bundles('webpack.config.skipCommon.js')
settings = {
@@ -489,7 +590,7 @@ def test_emits_warning_on_no_request_in_jinja2engine(self):
dups_template = jinja2_engine.get_template(
template_name='home-duplicated.jinja') # type: Jinja2Template
output = dups_template.render()
- _warn_mock.assert_not_called()
+ warn_mock.assert_not_called()
self.assertEqual(output.count(asset_app1), 2)
self.assertEqual(output.count(asset_app2), 2)
self.assertEqual(output.count(asset_vendor), 4)
@@ -504,13 +605,13 @@ def test_emits_warning_on_no_request_in_jinja2engine(self):
self.assertEqual(output.count(asset_app1), 2)
self.assertEqual(output.count(asset_app2), 2)
self.assertEqual(output.count(asset_vendor), 4)
- self.assertEqual(_warn_mock.call_count, 3)
+ self.assertEqual(warn_mock.call_count, 3)
self.assertListEqual(
- _warn_mock.call_args_list,
+ warn_mock.call_args_list,
[warning_call, warning_call, warning_call])
# Should NOT call `warn()` here
- _warn_mock.reset_mock()
+ warn_mock.reset_mock()
request = self.factory.get(path='/')
with self.settings(**settings):
jinja2_engine = engines['jinja2'] # type: Jinja2
@@ -518,14 +619,72 @@ def test_emits_warning_on_no_request_in_jinja2engine(self):
template_name='home-deduplicated.jinja'
) # type: Jinja2Template
output = nodups_template.render(request=request)
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 1)
- _warn_mock.assert_not_called()
- _warn_mock.reset_mock()
+ warn_mock.assert_not_called()
+ warn_mock.reset_mock()
+
+ @patch(target='webpack_loader.templatetags.webpack_loader.warn')
+ def test_get_files_emits_warning_on_no_request_in_djangoengine(self, warn_mock):
+ self.compile_bundles('webpack.config.skipCommon.js')
+ asset_vendor = '/static/django_webpack_loader_bundles/vendors.js'
+ asset_app1 = '/static/django_webpack_loader_bundles/app1.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" %}'
+ '{% get_files "app2" skip_common_chunks=True as app2_files %}'
+ '{% for f in app2_files %}'
+ ' '
+ '{% endfor %}'),
+ ) # type: Template
+ output = template.render(context=Context())
+ self.assertEqual(output.count(asset_app1), 1)
+ self.assertEqual(output.count(asset_app2), 1)
+ self.assertEqual(output.count(asset_vendor), 2)
+ warn_mock.assert_called_once_with(
+ message=_WARNING_MESSAGE, category=RuntimeWarning)
+
+ @patch(target='webpack_loader.templatetags.webpack_loader.warn')
+ def test_get_files_emits_warning_on_no_request_in_jinja2engine(self, warn_mock):
+ self.compile_bundles('webpack.config.skipCommon.js')
+ asset_vendor = '/static/django_webpack_loader_bundles/vendors.js'
+ asset_app1 = '/static/django_webpack_loader_bundles/app1.js'
+ asset_app2 = '/static/django_webpack_loader_bundles/app2.js'
+ settings = {
+ 'TEMPLATES': [
+ {
+ 'NAME': 'jinja2',
+ 'BACKEND': 'django_jinja.backend.Jinja2',
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'match_extension': '.jinja',
+ 'extensions': DEFAULT_EXTENSIONS + [_OUR_EXTENSION],
+ }
+ },
+ ]
+ }
+
+ with self.settings(**settings):
+ jinja2_engine = engines['jinja2'] # type: Jinja2
+ template = jinja2_engine.from_string(
+ "{{ render_bundle('app1', 'js') }}"
+ "{% set app2_files = webpack_get_files('app2', skip_common_chunks=True) %}"
+ "{% for f in app2_files %}"
+ " "
+ "{% endfor %}"
+ )
+ output = template.render(context=Context())
+ self.assertEqual(output.count(asset_app1), 1)
+ self.assertEqual(output.count(asset_app2), 1)
+ self.assertEqual(output.count(asset_vendor), 2)
+ warn_mock.assert_called_once_with(
+ message=_WARNING_MESSAGE, category=RuntimeWarning)
def _assert_common_chunks_duplicated_djangoengine(self, template):
"""
@@ -546,10 +705,10 @@ def _assert_common_chunks_duplicated_djangoengine(self, template):
'')
rendered_template = template.render(
context=None, request=request)
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
self.assertEqual(rendered_template.count(asset_app1), 1)
self.assertEqual(rendered_template.count(asset_app2), 1)
self.assertEqual(rendered_template.count(asset_vendor), 2)
@@ -573,10 +732,10 @@ def _assert_common_chunks_not_duplicated_djangoengine(self, template):
'')
rendered_template = template.render(
context=None, request=request)
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
self.assertEqual(rendered_template.count(asset_app1), 1)
self.assertEqual(rendered_template.count(asset_app2), 1)
self.assertEqual(rendered_template.count(asset_vendor), 1)
@@ -620,9 +779,9 @@ def _assert_common_chunks_duplicated_jinja2engine(self, view):
self.assertEqual(content.count(asset_vendor), 4)
self.assertEqual(content.count(asset_app1), 2)
self.assertEqual(content.count(asset_app2), 2)
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
def _assert_common_chunks_not_duplicated_jinja2engine(self, view):
"""
@@ -663,9 +822,9 @@ def _assert_common_chunks_not_duplicated_jinja2engine(self, view):
self.assertEqual(content.count(asset_vendor), 1)
self.assertEqual(content.count(asset_app1), 1)
self.assertEqual(content.count(asset_app2), 1)
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
- self.assertIsNotNone(used_tags, msg=(
- '_webpack_loader_used_tags should be a property of request!'))
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ self.assertIsNotNone(used_urls, msg=(
+ '_webpack_loader_used_urls should be a property of request!'))
def test_skip_common_chunks_templatetag_djangoengine(self):
"""Test case for deduplication of modules with the django engine via the render_bundle template tag."""
@@ -768,3 +927,95 @@ def test_skip_common_chunks_missing_config(self):
# return removed key
loader.config['SKIP_COMMON_CHUNKS'] = skip_common_chunks
+
+ 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')
+
+ self.assertEqual(len(tags), 3)
+ 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/assets/js/resource.txt b/tests_webpack5/assets/js/resource.txt
new file mode 100644
index 00000000..cd087558
--- /dev/null
+++ b/tests_webpack5/assets/js/resource.txt
@@ -0,0 +1 @@
+Hello world!
diff --git a/tests_webpack5/assets/js/resources.jsx b/tests_webpack5/assets/js/resources.jsx
new file mode 100644
index 00000000..2c6bc848
--- /dev/null
+++ b/tests_webpack5/assets/js/resources.jsx
@@ -0,0 +1 @@
+require("./resource.txt")
diff --git a/tests_webpack5/manage.py b/tests_webpack5/manage.py
new file mode 100755
index 00000000..72238252
--- /dev/null
+++ b/tests_webpack5/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/tests_webpack5/package-lock.json b/tests_webpack5/package-lock.json
new file mode 100644
index 00000000..ef75251d
--- /dev/null
+++ b/tests_webpack5/package-lock.json
@@ -0,0 +1,1347 @@
+{
+ "name": "example",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "example",
+ "version": "0.0.0",
+ "license": "MIT",
+ "devDependencies": {
+ "webpack": "^5.92.0",
+ "webpack-bundle-tracker": "3.2.1",
+ "webpack-cli": "^5.1.4"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.11.30",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
+ "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
+ "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+ "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
+ "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+ "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
+ "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.12.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+ "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+ "dev": true,
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+ "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+ "dev": true,
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
+ "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/helper-wasm-section": "1.12.1",
+ "@webassemblyjs/wasm-gen": "1.12.1",
+ "@webassemblyjs/wasm-opt": "1.12.1",
+ "@webassemblyjs/wasm-parser": "1.12.1",
+ "@webassemblyjs/wast-printer": "1.12.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
+ "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
+ "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
+ "@webassemblyjs/wasm-gen": "1.12.1",
+ "@webassemblyjs/wasm-parser": "1.12.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
+ "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
+ "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.12.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+ "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+ "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+ "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+ "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001587",
+ "electron-to-chromium": "^1.4.668",
+ "node-releases": "^2.0.14",
+ "update-browserslist-db": "^1.0.13"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001600",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
+ "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
+ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.721",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.721.tgz",
+ "integrity": "sha512-k1x2r6foI8iJOp+1qTxbbrrWMsOiHkzGBYwYigaq+apO1FSqtn44KTo3Sy69qt7CRr7149zTcsDvH7MUKsOuIQ==",
+ "dev": true
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.17.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+ "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.11.1",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz",
+ "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==",
+ "dev": true,
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz",
+ "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
+ "dev": true
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/schema-utils": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.30.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz",
+ "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.10",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
+ "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.20",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^3.1.1",
+ "serialize-javascript": "^6.0.1",
+ "terser": "^5.26.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
+ "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
+ "dev": true,
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.94.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
+ "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.5",
+ "@webassemblyjs/ast": "^1.12.1",
+ "@webassemblyjs/wasm-edit": "^1.12.1",
+ "@webassemblyjs/wasm-parser": "^1.12.1",
+ "acorn": "^8.7.1",
+ "acorn-import-attributes": "^1.9.5",
+ "browserslist": "^4.21.10",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.1",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^3.2.0",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.10",
+ "watchpack": "^2.4.1",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-bundle-tracker": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-3.2.1.tgz",
+ "integrity": "sha512-N1bi1Kok4kexXUvez2Jwrq2SpA4xm6JZfqK9L5hXNbe3E5pR4oS7UYgOq1ALapP89oRjHXc3fzblqDqo+zag2A==",
+ "dev": true
+ },
+ "node_modules/webpack-cli": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+ "dev": true,
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.5.0",
+ "@webpack-cli/configtest": "^2.1.1",
+ "@webpack-cli/info": "^2.0.2",
+ "@webpack-cli/serve": "^2.0.5",
+ "colorette": "^2.0.14",
+ "commander": "^10.0.1",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.7.3",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "@webpack-cli/generators": {
+ "optional": true
+ },
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli/node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
+ "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+ "dev": true,
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true
+ }
+ }
+}
diff --git a/tests_webpack5/package.json b/tests_webpack5/package.json
new file mode 100644
index 00000000..919f7a1f
--- /dev/null
+++ b/tests_webpack5/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "example",
+ "version": "0.0.0",
+ "description": "",
+ "main": "index.js",
+ "license": "MIT",
+ "devDependencies": {
+ "webpack": "^5.92.0",
+ "webpack-bundle-tracker": "3.2.1",
+ "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/tests_webpack5/webpack.config.js b/tests_webpack5/webpack.config.js
new file mode 100644
index 00000000..0f0c3522
--- /dev/null
+++ b/tests_webpack5/webpack.config.js
@@ -0,0 +1,26 @@
+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/'),
+ },
+
+ module: {
+ rules: [{ test: /\.txt$/, type: 'asset/resource' }]
+ },
+
+ plugins: [
+ new BundleTracker({path: __dirname})
+ ],
+
+ resolve: {
+ extensions: ['.js', '.jsx']
+ },
+}
+
diff --git a/tests_webpack5/webpack_loader b/tests_webpack5/webpack_loader
new file mode 120000
index 00000000..acec8a34
--- /dev/null
+++ b/tests_webpack5/webpack_loader
@@ -0,0 +1 @@
+../webpack_loader/
\ No newline at end of file
diff --git a/webpack_loader/__init__.py b/webpack_loader/__init__.py
index 73133ea7..b9692d73 100644
--- a/webpack_loader/__init__.py
+++ b/webpack_loader/__init__.py
@@ -1,5 +1,5 @@
__author__ = "Vinta Software"
-__version__ = "3.0.0"
+__version__ = "3.2.1"
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/contrib/jinja2ext.py b/webpack_loader/contrib/jinja2ext.py
index 3fbc9d15..e25e2935 100644
--- a/webpack_loader/contrib/jinja2ext.py
+++ b/webpack_loader/contrib/jinja2ext.py
@@ -2,7 +2,7 @@
from jinja2.runtime import Context
from jinja2.utils import pass_context
-from ..templatetags.webpack_loader import render_bundle
+from ..templatetags.webpack_loader import get_files, render_bundle
@pass_context
@@ -10,7 +10,13 @@ def _render_bundle(context: Context, *args, **kwargs):
return render_bundle(context, *args, **kwargs)
+@pass_context
+def _get_files(context: Context, *args, **kwargs):
+ return get_files(context, *args, **kwargs)
+
+
class WebpackExtension(Extension):
def __init__(self, environment):
super(WebpackExtension, self).__init__(environment)
environment.globals["render_bundle"] = _render_bundle
+ environment.globals["webpack_get_files"] = _get_files
diff --git a/webpack_loader/loaders.py b/webpack_loader/loaders.py
index 56d2f172..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 = {}
@@ -38,19 +67,73 @@ def get_assets(self):
return self._assets[self.name]
return self.load_assets()
- def get_integrity_attr(self, chunk):
- if not self.config.get("INTEGRITY"):
- return " "
-
- integrity = chunk.get("integrity")
+ 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 _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 f8a4890a..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
@@ -10,7 +12,7 @@
'You have specified skip_common_chunks=True but the passed context '
'doesn\'t have a request. django_webpack_loader needs a request object to '
'filter out duplicate chunks. Please see https://github.com/django-webpack'
- '/django-webpack-loader#skipping-the-generation-of-multiple-common-chunks')
+ '/django-webpack-loader#use-skip_common_chunks-on-render_bundle')
@register.simple_tag(takes_context=True)
@@ -19,21 +21,25 @@ def render_bundle(
attrs='', is_preload=False, skip_common_chunks=None):
if skip_common_chunks is None:
skip_common_chunks = utils.get_skip_common_chunks(config)
- tags = utils.get_as_tags(
- bundle_name, extension=extension, config=config, suffix=suffix,
- attrs=attrs, is_preload=is_preload)
- request = context.get('request')
+
+ 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)
+
if request is None:
if skip_common_chunks:
warn(message=_WARNING_MESSAGE, category=RuntimeWarning)
- return mark_safe('\n'.join(tags))
- used_tags = getattr(request, '_webpack_loader_used_tags', None)
- if not used_tags:
- used_tags = request._webpack_loader_used_tags = set()
+ return mark_safe('\n'.join(tags.values()))
+
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ if used_urls is None:
+ used_urls = set()
+ setattr(request, '_webpack_loader_used_urls', used_urls)
if skip_common_chunks:
- tags = [tag for tag in tags if tag not in used_tags]
- used_tags.update(tags)
- return mark_safe('\n'.join(tags))
+ 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
@@ -42,7 +48,14 @@ def webpack_static(asset_name, config='DEFAULT'):
@register.simple_tag
-def get_files(bundle_name, extension=None, config='DEFAULT'):
+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',
+ skip_common_chunks=None):
"""
Returns all chunks in the given bundle.
Example usage::
@@ -50,9 +63,27 @@ def get_files(bundle_name, extension=None, config='DEFAULT'):
{% get_files 'editor' 'css' as editor_css_chunks %}
CKEDITOR.config.contentsCss = '{{ editor_css_chunks.0.url }}';
+ :param context: The request, if you want to use `skip_common_chunks`
:param bundle_name: The name of the bundle
:param extension: (optional) filter by extension
:param config: (optional) the name of the configuration
+ :param skip_common_chunks: (optional) `True` if you want to skip returning already rendered common chunks
:return: a list of matching chunks
"""
- return utils.get_files(bundle_name, extension=extension, config=config)
+ if skip_common_chunks is None:
+ skip_common_chunks = utils.get_skip_common_chunks(config)
+
+ result = utils.get_files(bundle_name, extension=extension, config=config)
+
+ request = context.get('request')
+ if request is None:
+ if skip_common_chunks:
+ warn(message=_WARNING_MESSAGE, category=RuntimeWarning)
+ return result
+
+ used_urls = getattr(request, '_webpack_loader_used_urls', None)
+ if not used_urls:
+ used_urls = set()
+ if skip_common_chunks:
+ result = [chunk for chunk in result if chunk['url'] not in used_urls]
+ return result
diff --git a/webpack_loader/utils.py b/webpack_loader/utils.py
index ab5f151c..e789f70b 100644
--- a/webpack_loader/utils.py
+++ b/webpack_loader/utils.py
@@ -1,8 +1,12 @@
+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):
@@ -20,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):
@@ -56,45 +59,66 @@ def get_files(bundle_name, extension=None, config='DEFAULT'):
return list(_get_bundle(loader, bundle_name, extension))
-def get_as_tags(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 list of formatted '
+ result[chunk['url']] = (
+ ''
).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')):
- tags.append((
- ''
+ result[chunk['url']] = (
+ ''
).format(
''.join([chunk['url'], suffix]),
attrs,
'"stylesheet"' if not is_preload else '"preload" as="style"',
- loader.get_integrity_attr(chunk),
- ))
- return tags
+ loader.get_integrity_attr(chunk, request, attrs_l),
+ loader.get_nonce_attr(chunk, request, attrs_l),
+ )
+ return result
+
+
+def get_as_tags(
+ bundle_name, request=None, extension=None, config='DEFAULT', suffix='',
+ attrs='', is_preload=False):
+ '''
+ Get a list of formatted