Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ The `STATS_FILE` parameter represents the output file produced by `webpack-bundl

- `INTEGRITY` is flag enabling [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) on rendered `<script>` and `<link>` tags. Integrity hash is get from stats file and configuration on side of `BundleTracker`, where configuration option `integrity: true` is required.

- `LOADER_CLASS` is the fully qualified name of a python class as a string that holds the custom webpack loader. This is where behavior can be customized as to how the stats file is loaded. Examples include loading the stats file from a database, cache, external url, etc. For convenience, `webpack_loader.loader.WebpackLoader` can be extended. The `load_assets` method is likely where custom behavior will be added. This should return the stats file as an object.
- `LOADER_CLASS` is the fully qualified name of a python class as a string that holds the custom webpack loader. This is where behavior can be customized as to how the stats file is loaded. Examples include loading the stats file from a database, cache, external url, etc. For convenience, `webpack_loader.loaders.WebpackLoader` can be extended. The `load_assets` method is likely where custom behavior will be added. This should return the stats file as an object.

- `SKIP_COMMON_CHUNKS` is a flag which prevents already generated chunks from being included again in the same page. This should only happen if you use more than one entrypoint per Django template (multiple `render_bundle` calls). By enabling this, you can get the same default behavior of the [HtmlWebpackPlugin](https://webpack.js.org/plugins/html-webpack-plugin/). The same caveats apply as when using `skip_common_chunks` on `render_bundle`, see that section below for more details.

Here's a simple example of loading from an external url:

```py
import requests
from webpack_loader.loader import WebpackLoader
from webpack_loader.loaders import WebpackLoader

class ExternalWebpackLoader(WebpackLoader):
def load_assets(self):
Expand Down Expand Up @@ -183,7 +183,7 @@ However, production usage for this package is **fairly flexible**. Other approac
To run tests where `render_bundle` shows up, since we don't have `webpack-bundle-tracker` at that point to generate the stats file, the calls to render the bundle will fail. The solution is to use the `FakeWebpackLoader` in your test settings:

```python
WEBPACK_LOADER['DEFAULT']['LOADER_CLASS'] = 'webpack_loader.loader.FakeWebpackLoader'
WEBPACK_LOADER['DEFAULT']['LOADER_CLASS'] = 'webpack_loader.loaders.FakeWebpackLoader'
```

## Advanced Usage
Expand Down
4 changes: 2 additions & 2 deletions tests/app/tests/test_custom_loaders.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from importlib import reload
from django.test import TestCase
from webpack_loader import utils, config, loader
from webpack_loader import utils, config, loaders


DEFAULT_CONFIG = 'DEFAULT'
LOADER_PAYLOAD = {'status': 'done', 'chunks': []}


class ValidCustomLoader(loader.WebpackLoader):
class ValidCustomLoader(loaders.WebpackLoader):

def load_assets(self):
return LOADER_PAYLOAD
Expand Down
2 changes: 1 addition & 1 deletion webpack_loader/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader',
'LOADER_CLASS': 'webpack_loader.loaders.WebpackLoader',
'INTEGRITY': False,
# Whenever the global setting for SKIP_COMMON_CHUNKS is changed, please
# update the fallback value in get_skip_common_chunks (utils.py).
Expand Down
163 changes: 6 additions & 157 deletions webpack_loader/loader.py
Original file line number Diff line number Diff line change
@@ -1,160 +1,9 @@
import json
import time
import os
from io import open
import warnings

from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from .loaders import * # noqa

from .exceptions import (
WebpackError,
WebpackLoaderBadStatsError,
WebpackLoaderTimeoutError,
WebpackBundleLookupError
warnings.warn(
"The 'webpack_loader.loader' module has been renamed to 'webpack_loader.loaders'. "
"Please update your imports and config to use the new module.",
DeprecationWarning,
)


class WebpackLoader(object):
_assets = {}

def __init__(self, name, config):
self.name = name
self.config = config

def load_assets(self):
try:
with open(self.config['STATS_FILE'], encoding="utf-8") as f:
return json.load(f)
except IOError:
raise IOError(
'Error reading {0}. Are you sure webpack has generated '
'the file and the path is correct?'.format(
self.config['STATS_FILE']))

def get_assets(self):
if self.config['CACHE']:
if self.name not in self._assets:
self._assets[self.name] = self.load_assets()
return self._assets[self.name]
return self.load_assets()

def get_integrity_attr(self, chunk):
if not self.config.get('INTEGRITY'):
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?")

return ' integrity="{}" '.format(integrity.partition(' ')[0])

def filter_chunks(self, chunks):
filtered_chunks = []

for chunk in chunks:
ignore = any(regex.match(chunk)
for regex in self.config['ignores'])
if not ignore:
filtered_chunks.append(chunk)

return filtered_chunks

def map_chunk_files_to_url(self, chunks):
assets = self.get_assets()
files = assets['assets']

add_integrity = self.config.get('INTEGRITY')

for chunk in chunks:
url = self.get_chunk_url(files[chunk])

if add_integrity:
yield {'name': chunk, 'url': url, 'integrity': files[chunk].get('integrity')}
else:
yield {'name': chunk, 'url': url}

def get_chunk_url(self, chunk_file):
public_path = chunk_file.get('publicPath')
if public_path:
return public_path

# Use os.path.normpath for Windows paths
relpath = os.path.normpath(
os.path.join(self.config['BUNDLE_DIR_NAME'], chunk_file['name'])
)
return staticfiles_storage.url(relpath)

def get_bundle(self, bundle_name):
assets = self.get_assets()

# poll when debugging and block request until bundle is compiled
# or the build times out
if settings.DEBUG:
timeout = self.config['TIMEOUT'] or 0
timed_out = False
start = time.time()
while assets['status'] == 'compile' and not timed_out:
time.sleep(self.config['POLL_INTERVAL'])
if timeout and (time.time() - timeout > start):
timed_out = True
assets = self.get_assets()

if timed_out:
raise WebpackLoaderTimeoutError(
"Timed Out. Bundle `{0}` took more than {1} seconds "
"to compile.".format(bundle_name, timeout)
)

if assets.get('status') == 'done':
chunks = assets['chunks'].get(bundle_name, None)
if chunks is None:
raise WebpackBundleLookupError('Cannot resolve bundle {0}.'.format(bundle_name))

filtered_chunks = self.filter_chunks(chunks)

for chunk in filtered_chunks:
asset = assets['assets'][chunk]
if asset is None:
raise WebpackBundleLookupError('Cannot resolve asset {0}.'.format(chunk))

return self.map_chunk_files_to_url(filtered_chunks)

elif assets.get('status') == 'error':
if 'file' not in assets:
assets['file'] = ''
if 'error' not in assets:
assets['error'] = 'Unknown Error'
if 'message' not in assets:
assets['message'] = ''
error = u"""
{error} in {file}
{message}
""".format(**assets)
raise WebpackError(error)

raise WebpackLoaderBadStatsError(
"The stats file does not contain valid data. Make sure "
"webpack-bundle-tracker plugin is enabled and try to run "
"webpack again.")


class FakeWebpackLoader(WebpackLoader):
"""
A fake loader to help run Django tests.

For running tests where `render_bundle` is used but assets aren't built.
"""

def get_assets(self):
return {}

def get_bundle(self, _bundle_name):
return [
{
'name': 'test.bundle.js',
'url': 'http://localhost/static/bundles/test.bundle.js',
}
]
160 changes: 160 additions & 0 deletions webpack_loader/loaders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import json
import time
import os
from io import open

from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage

from .exceptions import (
WebpackError,
WebpackLoaderBadStatsError,
WebpackLoaderTimeoutError,
WebpackBundleLookupError
)


class WebpackLoader(object):
_assets = {}

def __init__(self, name, config):
self.name = name
self.config = config

def load_assets(self):
try:
with open(self.config['STATS_FILE'], encoding="utf-8") as f:
return json.load(f)
except IOError:
raise IOError(
'Error reading {0}. Are you sure webpack has generated '
'the file and the path is correct?'.format(
self.config['STATS_FILE']))

def get_assets(self):
if self.config['CACHE']:
if self.name not in self._assets:
self._assets[self.name] = self.load_assets()
return self._assets[self.name]
return self.load_assets()

def get_integrity_attr(self, chunk):
if not self.config.get('INTEGRITY'):
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?")

return ' integrity="{}" '.format(integrity.partition(' ')[0])

def filter_chunks(self, chunks):
filtered_chunks = []

for chunk in chunks:
ignore = any(regex.match(chunk)
for regex in self.config['ignores'])
if not ignore:
filtered_chunks.append(chunk)

return filtered_chunks

def map_chunk_files_to_url(self, chunks):
assets = self.get_assets()
files = assets['assets']

add_integrity = self.config.get('INTEGRITY')

for chunk in chunks:
url = self.get_chunk_url(files[chunk])

if add_integrity:
yield {'name': chunk, 'url': url, 'integrity': files[chunk].get('integrity')}
else:
yield {'name': chunk, 'url': url}

def get_chunk_url(self, chunk_file):
public_path = chunk_file.get('publicPath')
if public_path:
return public_path

# Use os.path.normpath for Windows paths
relpath = os.path.normpath(
os.path.join(self.config['BUNDLE_DIR_NAME'], chunk_file['name'])
)
return staticfiles_storage.url(relpath)

def get_bundle(self, bundle_name):
assets = self.get_assets()

# poll when debugging and block request until bundle is compiled
# or the build times out
if settings.DEBUG:
timeout = self.config['TIMEOUT'] or 0
timed_out = False
start = time.time()
while assets['status'] == 'compile' and not timed_out:
time.sleep(self.config['POLL_INTERVAL'])
if timeout and (time.time() - timeout > start):
timed_out = True
assets = self.get_assets()

if timed_out:
raise WebpackLoaderTimeoutError(
"Timed Out. Bundle `{0}` took more than {1} seconds "
"to compile.".format(bundle_name, timeout)
)

if assets.get('status') == 'done':
chunks = assets['chunks'].get(bundle_name, None)
if chunks is None:
raise WebpackBundleLookupError('Cannot resolve bundle {0}.'.format(bundle_name))

filtered_chunks = self.filter_chunks(chunks)

for chunk in filtered_chunks:
asset = assets['assets'][chunk]
if asset is None:
raise WebpackBundleLookupError('Cannot resolve asset {0}.'.format(chunk))

return self.map_chunk_files_to_url(filtered_chunks)

elif assets.get('status') == 'error':
if 'file' not in assets:
assets['file'] = ''
if 'error' not in assets:
assets['error'] = 'Unknown Error'
if 'message' not in assets:
assets['message'] = ''
error = u"""
{error} in {file}
{message}
""".format(**assets)
raise WebpackError(error)

raise WebpackLoaderBadStatsError(
"The stats file does not contain valid data. Make sure "
"webpack-bundle-tracker plugin is enabled and try to run "
"webpack again.")


class FakeWebpackLoader(WebpackLoader):
"""
A fake loader to help run Django tests.

For running tests where `render_bundle` is used but assets aren't built.
"""

def get_assets(self):
return {}

def get_bundle(self, _bundle_name):
return [
{
'name': 'test.bundle.js',
'url': 'http://localhost/static/bundles/test.bundle.js',
}
]