From 1d0553b9389eab1972a3b72130d5ba5a4a2f54f3 Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Mon, 5 Oct 2020 16:29:52 -0700 Subject: [PATCH 01/15] allow pattern matching to render multiple assets --- easy_django_webpack/models.py | 3 - easy_django_webpack/templatetags/manifest.py | 127 ++++++++++++++----- easy_django_webpack/views.py | 0 3 files changed, 97 insertions(+), 33 deletions(-) delete mode 100644 easy_django_webpack/models.py delete mode 100644 easy_django_webpack/views.py diff --git a/easy_django_webpack/models.py b/easy_django_webpack/models.py deleted file mode 100644 index 71a8362..0000000 --- a/easy_django_webpack/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/easy_django_webpack/templatetags/manifest.py b/easy_django_webpack/templatetags/manifest.py index 824e6a6..1cba3be 100644 --- a/easy_django_webpack/templatetags/manifest.py +++ b/easy_django_webpack/templatetags/manifest.py @@ -1,15 +1,16 @@ import json import os +import fnmatch from django import template -from django.templatetags.static import do_static +from django.templatetags.static import do_static, StaticNode from django.conf import settings from django.core.cache import cache register = template.Library() APP_SETTINGS = { - 'output_dir': settings.BASE_DIR / 'dist', + 'output_dir': None, 'manifest_file': 'manifest.json', 'cache': False, 'ignore_missing_assets': False, @@ -20,34 +21,99 @@ @register.tag('manifest') -def manifest(parser, token): - cached_manifest = cache.get('webpack_manifest') - path = os.path.join(APP_SETTINGS['output_dir'], - APP_SETTINGS['manifest_file']) - - if APP_SETTINGS['cache'] and cached_manifest: - data = cached_manifest - else: - try: - with open(path) as manifest_file: - data = json.load(manifest_file) - except FileNotFoundError: - raise WebpackManifestNotFound(path) - - if APP_SETTINGS['cache']: - cache.set('webpack_manifest', data) +def do_manifest(parser, token): + tag_name, filename = parse_token(token) + manifest = get_manifest() - hashed_filename = data.get(parse_filename(token)) + hashed_filename = manifest.get(filename) if hashed_filename: token.contents = "webpack '{}'".format(hashed_filename) elif not APP_SETTINGS['ignore_missing_assets']: - raise AssetNotFoundInWebpackManifest(parse_filename(token), path) + raise AssetNotFoundInWebpackManifest(filename) return do_static(parser, token) -def parse_filename(token): - return token.contents.split("'")[1] +@register.tag('manifest_match') +def do_manifest_match(parser, token): + return ManifestNode(parser, token) + + +class ManifestNode(template.Node): + def __init__(self, parser, token): + tag_name, self.search_string, self.output_tag = parse_token(token) + self.manifest = get_manifest() + self.parser = parser + self.token = token + self.matched_files = [file for file in self.manifest.keys() if + fnmatch.fnmatch(file, self.search_string)] + self.mapped_files = [self.manifest.get(file) for file in self.matched_files] + + def render(self, context): + urls = [] + for file in self.mapped_files: + self.token.contents = "manifest_match '{}'".format(file) + node = StaticNode.handle_token(self.parser, self.token) + url = node.render(context) + urls.append(url) + output_tags = [self.output_tag.format(match=file) for file in urls] + return '\n'.join(output_tags) + + +def get_manifest(): + cached_manifest = cache.get('webpack_manifest') + if APP_SETTINGS['cache'] and cached_manifest: + return cached_manifest + + if APP_SETTINGS['output_dir']: + manifest_path = os.path.join(APP_SETTINGS['output_dir'], + APP_SETTINGS['manifest_file']) + else: + manifest_path = find_manifest_path() + + try: + with open(manifest_path) as manifest_file: + data = json.load(manifest_file) + except FileNotFoundError: + raise WebpackManifestNotFound(manifest_path) + + if APP_SETTINGS['cache']: + cache.set('webpack_manifest', data) + + return data + + +def find_manifest_path(): + static_dirs = settings.STATICFILES_DIRS + if len(static_dirs) == 1: + return os.path.join(static_dirs[0], APP_SETTINGS['manifest_file']) + for static_dir in static_dirs: + manifest_path = os.path.join(static_dir, APP_SETTINGS['manifest_file']) + if os.path.isfile(manifest_path): + return manifest_path + raise WebpackManifestNotFound('settings.STATICFILES_DIRS') + + +def parse_token(token): + contents = token.split_contents() + if len(contents) == 2: + tag_name, file_name = contents + return tag_name, strip_quotes(tag_name, file_name) + elif len(contents) == 3: + tag_name, match_string, output_tag = contents + return tag_name, strip_quotes(tag_name, match_string), strip_quotes(tag_name, output_tag) + raise template.TemplateSyntaxError( + "%r tag given the wrong number of arguments" % token.contents.split()[0] + ) + + +def strip_quotes(tag_name, content): + if not (content[0] == content[-1] and + content[0] in ('"', "'")): + raise template.TemplateSyntaxError( + "%r tag's argument should be in quotes" % tag_name + ) + return content[1:-1] class WebpackManifestNotFound(Exception): @@ -59,11 +125,12 @@ def __init__(self, path, message='Manifest file named {} not found. ' class AssetNotFoundInWebpackManifest(Exception): - def __init__(self, file, path, message='File {} is not referenced in the ' - 'manifest file located at {}. Make ' - 'sure webpack is outputting it. If ' - 'you would like to suppress this ' - 'error set WEBPACK_SETTINGS[' - '"ignore_missing_assets"] ' - 'to True'): - super().__init__(message.format(file, path)) + def __init__(self, file, message='File {} is not referenced in the ' + 'manifest file. Make ' + 'sure webpack is outputting it or try ' + 'disabling the cache if enabled. If ' + 'you would like to suppress this ' + 'error set WEBPACK_SETTINGS[' + '"ignore_missing_assets"] ' + 'to True'): + super().__init__(message.format(file)) diff --git a/easy_django_webpack/views.py b/easy_django_webpack/views.py deleted file mode 100644 index e69de29..0000000 From 81b66403ddf74274ec45606c0626b9c6b97122aa Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Mon, 5 Oct 2020 18:32:00 -0700 Subject: [PATCH 02/15] rename installable app and establish testing framework --- easy_django_webpack/apps.py | 5 ----- easy_django_webpack/tests.py | 3 --- .../__init__.py | 0 manifest_loader/apps.py | 5 +++++ .../templatetags/__init__.py | 0 .../templatetags/manifest.py | 0 runtests.py | 15 +++++++++++++++ tests/__init__.py | 0 tests/test_settings.py | 4 ++++ tests/tests.py | 6 ++++++ 10 files changed, 30 insertions(+), 8 deletions(-) delete mode 100644 easy_django_webpack/apps.py delete mode 100644 easy_django_webpack/tests.py rename {easy_django_webpack => manifest_loader}/__init__.py (100%) create mode 100644 manifest_loader/apps.py rename {easy_django_webpack => manifest_loader}/templatetags/__init__.py (100%) rename {easy_django_webpack => manifest_loader}/templatetags/manifest.py (100%) create mode 100755 runtests.py create mode 100644 tests/__init__.py create mode 100644 tests/test_settings.py create mode 100644 tests/tests.py diff --git a/easy_django_webpack/apps.py b/easy_django_webpack/apps.py deleted file mode 100644 index c5452d2..0000000 --- a/easy_django_webpack/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class EasyDjangoWebpackConfig(AppConfig): - name = 'easy_django_webpack' diff --git a/easy_django_webpack/tests.py b/easy_django_webpack/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/easy_django_webpack/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/easy_django_webpack/__init__.py b/manifest_loader/__init__.py similarity index 100% rename from easy_django_webpack/__init__.py rename to manifest_loader/__init__.py diff --git a/manifest_loader/apps.py b/manifest_loader/apps.py new file mode 100644 index 0000000..89df6db --- /dev/null +++ b/manifest_loader/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ManifestLoader(AppConfig): + name = 'manifest_loader' diff --git a/easy_django_webpack/templatetags/__init__.py b/manifest_loader/templatetags/__init__.py similarity index 100% rename from easy_django_webpack/templatetags/__init__.py rename to manifest_loader/templatetags/__init__.py diff --git a/easy_django_webpack/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py similarity index 100% rename from easy_django_webpack/templatetags/manifest.py rename to manifest_loader/templatetags/manifest.py diff --git a/runtests.py b/runtests.py new file mode 100755 index 0000000..5fbfd98 --- /dev/null +++ b/runtests.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +import django +from django.conf import settings +from django.test.utils import get_runner + +if __name__ == "__main__": + os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' + django.setup() + TestRunner = get_runner(settings) + test_runner = TestRunner() + failures = test_runner.run_tests(['tests']) + sys.exit(bool(failures)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..27799c0 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,4 @@ +SECRET_KEY = 'fake-key' +INSTALLED_APPS = [ + "tests", +] diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..70e3363 --- /dev/null +++ b/tests/tests.py @@ -0,0 +1,6 @@ +from django.test import SimpleTestCase + + +class FooBarTests(SimpleTestCase): + def test_foo(self): + self.assertTrue(True) From 61a4ef4f1d9782e1ab84498845bf192d91fda70d Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Mon, 5 Oct 2020 18:35:19 -0700 Subject: [PATCH 03/15] rename importable app in example project and readme --- README.md | 4 ++-- example_project/frontend/templates/frontend/index.html | 4 +++- example_project/project/settings.py | 8 ++++---- manifest_loader/templatetags/manifest.py | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f7f0114..ea9c866 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ pip install easy_django_webpack INSTALLED_APPS = [ ... - 'easy_django_webpack', + 'manifest_loader', ... ] ``` @@ -53,7 +53,7 @@ you shouldn't be modifying it. _Hint: the `BASE_DIR` is the directory your `mana ```python # settings.py -WEBPACK_SETTINGS = { +MANIFEST_LOADER_SETTINGS = { 'output_dir': BASE_DIR / 'dist', # where webpack outputs to. 'manifest_file': 'manifest.json', # name of your manifest file 'cache': False, # recommended True for production, requires a server restart to pickup new values from the manifest. diff --git a/example_project/frontend/templates/frontend/index.html b/example_project/frontend/templates/frontend/index.html index da65fd0..e3af370 100644 --- a/example_project/frontend/templates/frontend/index.html +++ b/example_project/frontend/templates/frontend/index.html @@ -1,4 +1,6 @@ {% load manifest %}

- + +{% manifest_match '*.js' '' %} + diff --git a/example_project/project/settings.py b/example_project/project/settings.py index 10d926e..e1ade7b 100644 --- a/example_project/project/settings.py +++ b/example_project/project/settings.py @@ -37,7 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'easy_django_webpack', + 'manifest_loader', 'frontend', ] @@ -122,11 +122,11 @@ STATIC_URL = '/static/' STATICFILES_DIRS = [ - BASE_DIR / 'dist' + BASE_DIR / 'dist', ] -WEBPACK_SETTINGS = { - 'output_dir': BASE_DIR / 'dist', +MANIFEST_LOADER_SETTINGS = { + # 'output_dir': BASE_DIR / 'dist', 'manifest_file': 'manifest.json', 'cache': True, 'ignore_missing_assets': True, diff --git a/manifest_loader/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py index 1cba3be..0176553 100644 --- a/manifest_loader/templatetags/manifest.py +++ b/manifest_loader/templatetags/manifest.py @@ -16,8 +16,8 @@ 'ignore_missing_assets': False, } -if hasattr(settings, 'WEBPACK_SETTINGS'): - APP_SETTINGS.update(settings.WEBPACK_SETTINGS) +if hasattr(settings, 'MANIFEST_LOADER_SETTINGS'): + APP_SETTINGS.update(settings.MANIFEST_LOADER_SETTINGS) @register.tag('manifest') From b0c1a2382976e06f3c3ff0a1068bc6380835863e Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Mon, 5 Oct 2020 21:49:58 -0700 Subject: [PATCH 04/15] tests --- .gitignore | 3 +- manifest_loader/templatetags/manifest.py | 10 ++- tests/dist/main.e12dfe2f9b185dea03a4.js | 1 + tests/dist/manifest.json | 3 + tests/test_settings.py | 14 +++- tests/tests.py | 86 +++++++++++++++++++++++- 6 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 tests/dist/main.e12dfe2f9b185dea03a4.js create mode 100644 tests/dist/manifest.json diff --git a/.gitignore b/.gitignore index 53c0b0c..7352cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ __pycache__ dist node_modules *.egg-info -build \ No newline at end of file +build +!tests/dist \ No newline at end of file diff --git a/manifest_loader/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py index 0176553..ef2d03c 100644 --- a/manifest_loader/templatetags/manifest.py +++ b/manifest_loader/templatetags/manifest.py @@ -16,8 +16,8 @@ 'ignore_missing_assets': False, } -if hasattr(settings, 'MANIFEST_LOADER_SETTINGS'): - APP_SETTINGS.update(settings.MANIFEST_LOADER_SETTINGS) +if hasattr(settings, 'MANIFEST_LOADER'): + APP_SETTINGS.update(settings.MANIFEST_LOADER) @register.tag('manifest') @@ -108,6 +108,10 @@ def parse_token(token): def strip_quotes(tag_name, content): + if not isinstance(content, str): + raise template.TemplateSyntaxError( + "%r tag's argument should be a string in quotes" + ) if not (content[0] == content[-1] and content[0] in ('"', "'")): raise template.TemplateSyntaxError( @@ -119,7 +123,7 @@ def strip_quotes(tag_name, content): class WebpackManifestNotFound(Exception): def __init__(self, path, message='Manifest file named {} not found. ' 'Looked for it at {}. Either your ' - 'settings are wrong or you need to still ' + 'settings are wrong or you still need to ' 'generate the file.'): super().__init__(message.format(APP_SETTINGS['manifest_file'], path)) diff --git a/tests/dist/main.e12dfe2f9b185dea03a4.js b/tests/dist/main.e12dfe2f9b185dea03a4.js new file mode 100644 index 0000000..9ca9485 --- /dev/null +++ b/tests/dist/main.e12dfe2f9b185dea03a4.js @@ -0,0 +1 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){document.getElementById("main").innerText="It works!"}]); \ No newline at end of file diff --git a/tests/dist/manifest.json b/tests/dist/manifest.json new file mode 100644 index 0000000..d116ff0 --- /dev/null +++ b/tests/dist/manifest.json @@ -0,0 +1,3 @@ +{ + "main.js": "main.e12dfe2f9b185dea03a4.js" +} \ No newline at end of file diff --git a/tests/test_settings.py b/tests/test_settings.py index 27799c0..15aec17 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,4 +1,16 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent / 'tests' + SECRET_KEY = 'fake-key' + INSTALLED_APPS = [ - "tests", + 'manifest_loader', + 'tests', ] + +STATICFILES_DIRS = [ + BASE_DIR / 'dist' +] + +MANIFEST_LOADER = {} diff --git a/tests/tests.py b/tests/tests.py index 70e3363..0c8fc4b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,6 +1,86 @@ +from django.conf import settings from django.test import SimpleTestCase +from django.template import TemplateSyntaxError +from django.core.cache import cache +from manifest_loader.templatetags.manifest import strip_quotes, \ + find_manifest_path, WebpackManifestNotFound, get_manifest, APP_SETTINGS -class FooBarTests(SimpleTestCase): - def test_foo(self): - self.assertTrue(True) +NEW_STATICFILES_DIRS = [ + settings.BASE_DIR / 'foo', + settings.BASE_DIR / 'bar' / 'baz', + settings.BASE_DIR / 'dist', + settings.BASE_DIR / 'bax', +] + + +class StripQuotesTests(SimpleTestCase): + def test_can_remove_quotes(self): + self.assertEqual( + strip_quotes('test_case', "'foobar'"), + 'foobar' + ) + self.assertEqual( + strip_quotes('test_case', '"foobar"'), + 'foobar' + ) + + def test_will_raise_exception(self): + with self.assertRaises(TemplateSyntaxError): + strip_quotes('test_case', 'foobar') + with self.assertRaises(TemplateSyntaxError): + strip_quotes('test_case', 'foobar"') + with self.assertRaises(TemplateSyntaxError): + strip_quotes('test_case', 1234) + with self.assertRaises(TemplateSyntaxError): + strip_quotes('test_case', {'doo': 'bar'}) + + +class FindManifestPathTests(SimpleTestCase): + def test_default_path_found(self): + self.assertEqual( + str(settings.BASE_DIR / 'dist' / 'manifest.json'), + find_manifest_path() + ) + + def test_correct_path_found_among_options(self): + with self.settings(STATICFILES_DIRS=NEW_STATICFILES_DIRS): + self.assertEqual(len(settings.STATICFILES_DIRS), 4) + self.assertEqual( + str(settings.BASE_DIR / 'dist' / 'manifest.json'), + find_manifest_path() + ) + + def test_manifest_not_found(self): + with self.settings(STATICFILES_DIRS=[settings.BASE_DIR / 'foo', + settings.BASE_DIR / 'bar']): + self.assertEqual(len(settings.STATICFILES_DIRS), 2) + with self.assertRaises(WebpackManifestNotFound): + find_manifest_path() + + def test_manifest_not_found_empty(self): + with self.settings(STATICFILES_DIRS=[]): + self.assertEqual(len(settings.STATICFILES_DIRS), 0) + with self.assertRaises(WebpackManifestNotFound): + find_manifest_path() + + +class GetManifestTests(SimpleTestCase): + def test_cached_manifest(self): + cache.set('webpack_manifest', {'foo': 'bar'}) + APP_SETTINGS.update({'cache': True}) + self.assertDictEqual( + {'foo': 'bar'}, + get_manifest() + ) + cache.delete('webpack_manifest') + APP_SETTINGS.update({'cache': False}) + + def test_cached_not_used(self): + cache.set('webpack_manifest', {'foo': 'bar'}) + self.assertFalse(APP_SETTINGS['cache']) + self.assertDictEqual( + {'main.js': 'main.e12dfe2f9b185dea03a4.js'}, + get_manifest() + ) + cache.delete('webpack_manifest') From c213bbb8246ebaa217223821acc17c5f0dc5bae4 Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Mon, 5 Oct 2020 22:09:02 -0700 Subject: [PATCH 05/15] test most utils --- manifest_loader/templatetags/manifest.py | 3 +++ tests/tests.py | 29 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/manifest_loader/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py index ef2d03c..f4d78f0 100644 --- a/manifest_loader/templatetags/manifest.py +++ b/manifest_loader/templatetags/manifest.py @@ -61,6 +61,7 @@ def render(self, context): def get_manifest(): + """has test coverage""" cached_manifest = cache.get('webpack_manifest') if APP_SETTINGS['cache'] and cached_manifest: return cached_manifest @@ -84,6 +85,7 @@ def get_manifest(): def find_manifest_path(): + """has test coverage""" static_dirs = settings.STATICFILES_DIRS if len(static_dirs) == 1: return os.path.join(static_dirs[0], APP_SETTINGS['manifest_file']) @@ -108,6 +110,7 @@ def parse_token(token): def strip_quotes(tag_name, content): + """has test coverage""" if not isinstance(content, str): raise template.TemplateSyntaxError( "%r tag's argument should be a string in quotes" diff --git a/tests/tests.py b/tests/tests.py index 0c8fc4b..3d6ed20 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -76,11 +76,38 @@ def test_cached_manifest(self): cache.delete('webpack_manifest') APP_SETTINGS.update({'cache': False}) - def test_cached_not_used(self): + def test_cache_not_used(self): cache.set('webpack_manifest', {'foo': 'bar'}) self.assertFalse(APP_SETTINGS['cache']) self.assertDictEqual( {'main.js': 'main.e12dfe2f9b185dea03a4.js'}, get_manifest() ) + self.assertDictEqual( + {'foo': 'bar'}, + cache.get('webpack_manifest') + ) + cache.delete('webpack_manifest') + + def test_custom_output_dir(self): + APP_SETTINGS.update({'output_dir': settings.BASE_DIR / 'foo'}) + with self.assertRaises(WebpackManifestNotFound): + get_manifest() + APP_SETTINGS.update({'output_dir': None}) + + def test_cache_set(self): + APP_SETTINGS.update({'cache': True}) + self.assertIsNone(cache.get('webpack_manifest')) + manifest = get_manifest() + + self.assertDictEqual( + manifest, + {'main.js': 'main.e12dfe2f9b185dea03a4.js'} + ) + + self.assertDictEqual( + manifest, + cache.get('webpack_manifest') + ) + APP_SETTINGS.update({'cache': False}) cache.delete('webpack_manifest') From f0ae10cbcb51351fc4187d91f31b7663a3e7470d Mon Sep 17 00:00:00 2001 From: Elliot Charney Date: Tue, 6 Oct 2020 08:43:39 -0700 Subject: [PATCH 06/15] finish test coverage --- manifest_loader/templatetags/manifest.py | 19 +++- tests/dist/chunk1.hash.js | 0 tests/dist/chunk2.hash.js | 0 tests/dist/chunk3.hash.js | 0 tests/dist/manifest.json | 6 +- tests/dist/styles.hash.css | 0 tests/test_settings.py | 18 +++ tests/tests.py | 136 ++++++++++++++++++++++- 8 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 tests/dist/chunk1.hash.js create mode 100644 tests/dist/chunk2.hash.js create mode 100644 tests/dist/chunk3.hash.js create mode 100644 tests/dist/styles.hash.css diff --git a/manifest_loader/templatetags/manifest.py b/manifest_loader/templatetags/manifest.py index f4d78f0..e5ab6f4 100644 --- a/manifest_loader/templatetags/manifest.py +++ b/manifest_loader/templatetags/manifest.py @@ -22,7 +22,14 @@ @register.tag('manifest') def do_manifest(parser, token): - tag_name, filename = parse_token(token) + try: + tag_name, filename = parse_token(token) + except ValueError: + raise template.TemplateSyntaxError( + "%r tag given the wrong number of arguments" % + token.contents.split()[0] + ) + manifest = get_manifest() hashed_filename = manifest.get(filename) @@ -41,7 +48,15 @@ def do_manifest_match(parser, token): class ManifestNode(template.Node): def __init__(self, parser, token): - tag_name, self.search_string, self.output_tag = parse_token(token) + + try: + tag_name, self.search_string, self.output_tag = parse_token(token) + except ValueError: + raise template.TemplateSyntaxError( + "%r tag given the wrong number of arguments" % + token.contents.split()[0] + ) + self.manifest = get_manifest() self.parser = parser self.token = token diff --git a/tests/dist/chunk1.hash.js b/tests/dist/chunk1.hash.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/dist/chunk2.hash.js b/tests/dist/chunk2.hash.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/dist/chunk3.hash.js b/tests/dist/chunk3.hash.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/dist/manifest.json b/tests/dist/manifest.json index d116ff0..67f6e96 100644 --- a/tests/dist/manifest.json +++ b/tests/dist/manifest.json @@ -1,3 +1,7 @@ { - "main.js": "main.e12dfe2f9b185dea03a4.js" + "main.js": "main.e12dfe2f9b185dea03a4.js", + "chunk1.js": "chunk1.hash.js", + "chunk2.js": "chunk2.hash.js", + "chunk3.js": "chunk3.hash.js", + "styles.css": "styles.hash.css" } \ No newline at end of file diff --git a/tests/dist/styles.hash.css b/tests/dist/styles.hash.css new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_settings.py b/tests/test_settings.py index 15aec17..fd34c88 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -9,8 +9,26 @@ 'tests', ] +STATIC_URL = '/static/' + STATICFILES_DIRS = [ BASE_DIR / 'dist' ] MANIFEST_LOADER = {} + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] \ No newline at end of file diff --git a/tests/tests.py b/tests/tests.py index 3d6ed20..fb2255e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,10 +1,12 @@ from django.conf import settings from django.test import SimpleTestCase -from django.template import TemplateSyntaxError +from django.template import TemplateSyntaxError, Context, Template from django.core.cache import cache from manifest_loader.templatetags.manifest import strip_quotes, \ - find_manifest_path, WebpackManifestNotFound, get_manifest, APP_SETTINGS + find_manifest_path, WebpackManifestNotFound, get_manifest, APP_SETTINGS, \ + AssetNotFoundInWebpackManifest + NEW_STATICFILES_DIRS = [ settings.BASE_DIR / 'foo', @@ -14,6 +16,12 @@ ] +def render_template(string, context=None): + context = context or {} + context = Context(context) + return Template(string).render(context) + + class StripQuotesTests(SimpleTestCase): def test_can_remove_quotes(self): self.assertEqual( @@ -80,7 +88,13 @@ def test_cache_not_used(self): cache.set('webpack_manifest', {'foo': 'bar'}) self.assertFalse(APP_SETTINGS['cache']) self.assertDictEqual( - {'main.js': 'main.e12dfe2f9b185dea03a4.js'}, + { + 'main.js': 'main.e12dfe2f9b185dea03a4.js', + "chunk1.js": "chunk1.hash.js", + "chunk2.js": "chunk2.hash.js", + "chunk3.js": "chunk3.hash.js", + "styles.css": "styles.hash.css" + }, get_manifest() ) self.assertDictEqual( @@ -102,7 +116,13 @@ def test_cache_set(self): self.assertDictEqual( manifest, - {'main.js': 'main.e12dfe2f9b185dea03a4.js'} + { + 'main.js': 'main.e12dfe2f9b185dea03a4.js', + "chunk1.js": "chunk1.hash.js", + "chunk2.js": "chunk2.hash.js", + "chunk3.js": "chunk3.hash.js", + "styles.css": "styles.hash.css" + }, ) self.assertDictEqual( @@ -111,3 +131,111 @@ def test_cache_set(self): ) APP_SETTINGS.update({'cache': False}) cache.delete('webpack_manifest') + + +class ManifestTagTests(SimpleTestCase): + def test_basic_usage(self): + rendered = render_template( + '{% load manifest %}' + '{% manifest "main.js" %}' + ) + self.assertEqual( + rendered, + '/static/main.e12dfe2f9b185dea03a4.js' + ) + + def test_non_default_static_url(self): + with self.settings(STATIC_URL='/foo/'): + rendered = render_template( + '{% load manifest %}' + '{% manifest "main.js" %}' + ) + self.assertEqual( + rendered, + '/foo/main.e12dfe2f9b185dea03a4.js' + ) + + def test_no_arg(self): + with self.assertRaises(TemplateSyntaxError): + render_template( + '{% load manifest %}' + '{% manifest %}' + ) + + def test_too_many_args(self): + with self.assertRaises(TemplateSyntaxError): + render_template( + '{% load manifest %}' + '{% manifest "foo" "bar" %}' + ) + + def test_missing_asset(self): + with self.assertRaises(AssetNotFoundInWebpackManifest): + render_template( + '{% load manifest %}' + '{% manifest "foo.js" %}' + ) + + def test_ignore_missing_assets(self): + APP_SETTINGS.update({'ignore_missing_assets': True}) + rendered = render_template( + '{% load manifest %}' + '{% manifest "foo.js" %}' + ) + self.assertEqual( + rendered, + '/static/foo.js' + ) + APP_SETTINGS.update({'ignore_missing_assets': False}) + + +class ManifestMatchTagTests(SimpleTestCase): + def test_renders_correctly(self): + rendered = render_template( + '{% load manifest %}' + '{% manifest_match "*.css" "" %}' + ) + self.assertEqual( + rendered, + '' + ) + + def test_handles_no_math(self): + rendered = render_template( + '{% load manifest %}' + '{% manifest_match "*.exe" "" %}' + ) + self.assertEqual( + rendered, + '' + ) + + def test_renders_multiple_files(self): + rendered = render_template( + '{% load manifest %}' + "{% manifest_match '*.js' ' +{% load manifest %} + + ``` -## About +Where the argument to the tag will be the original filename of a file processed by webpack. If in doubt, check your +`manifest.json` file generated by webpack to see what files are available. + +The reason this is worth while is because of the content hash after the original filename, which will invalidate the +browser cache every time the file is updated. This ensures that your users always have the latest assets. + +## Split chunks (`manifest_match` tag) + +```djangotemplate +{% load manifest %} + + +``` + +turns into + +```html +