├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── bar │ └── woz.js ├── foo │ ├── bar.css │ └── bar.js ├── test_manifest_1.json ├── test_manifest_2.json ├── test_manifest_3.json ├── test_manifest_4.json ├── test_webpack_manifest.py └── woz │ ├── bar.css │ └── bar.js └── webpack_manifest ├── __init__.py ├── templatetags ├── __init__.py └── webpack_manifest_tags.py └── webpack_manifest.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | .idea 60 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ### 2.1.1 (29/08/2018) 5 | 6 | - Fixed a bug preventing `debug` flag from being passed to the Django template tag. 7 | 8 | 9 | ### 2.1.0 (26/07/2018) 10 | 11 | - Added Django template tag to load manifests during template runtime. 12 | 13 | 14 | ### 2.0.0 (25/07/2018) 15 | 16 | - Exceptions will now be raised if an unknown entry is accessed. See [https://github.com/markfinger/python-webpack-manifest/issues/1] 17 | - Improved ability to debug `WebpackManifestTypeEntry` instances as they can now access their parent manifest via `self.manifest`. 18 | 19 | ### 1.2.0 (13/02/2017) 20 | 21 | - Support inline rendering via @IlyaSemenov [https://github.com/markfinger/python-webpack-manifest/pull/5] 22 | 23 | ### 1.1.0 (19/12/2016) 24 | 25 | - Python 3 compatibility fixes from @IlyaSemenov [https://github.com/markfinger/python-webpack-manifest/pull/3] 26 | 27 | ### 1.0.0 (21/4/2016) 28 | 29 | - Improving handling of write-buffer race conditions. 30 | 31 | ### 0.3.0 (22/9/2015) 32 | 33 | - Fixed an issue where a write-buffer race condition emerges between the node and python processes 34 | 35 | ### 0.2.0 (1/9/2015) 36 | 37 | - Initial Release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mark Finger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-webpack-manifest 2 | ======================= 3 | 4 | Manifest loader that allows you to include references to files built by webpack. Handles manifests generated by the [webpack-yam-plugin](https://github.com/markfinger/webpack-yam-plugin). 5 | 6 | - Load references to webpack assets 7 | - Uses relative paths to ensure that manifests+assets can be pre-built and deployed across environments 8 | - Caches file reads to reduce overhead in production environments 9 | - Provides an opt-in debug mode which disables caching and blocks the python process as webpack completes re-builds 10 | - Designed to be optionally packaged with redistributable apps+libraries that need to avoid dependency-hell. 11 | 12 | If you are using webpack with Django, you might also want to check out Owais' [django-webpack-loader](https://github.com/owais/django-webpack-loader/) project. He has some great docs and the project has a lot of use. This project originated as a re-implementation of django-webpack-loader as I needed support for some of the above features. Personally, I continue to use this project in Django projects by exposing the manifest to templates via a call to `webpack_manifest.load(...)` in a view or a context processor. 13 | 14 | 15 | Documentation 16 | ------------- 17 | 18 | - [Installation](#installation) 19 | - [Usage](#usage) 20 | - [Usage in Django](#usage-in-django) 21 | - [How to run the tests](#how-to-run-the-tests) 22 | 23 | 24 | Installation 25 | ------------ 26 | 27 | If you're using this in a project, use `pip` 28 | 29 | ``` 30 | pip install webpack-manifest 31 | ``` 32 | 33 | If you're using this in an redistributable app or library, just copy the [loader's file](webpack_manifest/webpack_manifest.py) 34 | in so that you can avoid causing any dependency pains downstream. 35 | 36 | 37 | Usage 38 | ----- 39 | 40 | If you installed with pip, import it with 41 | 42 | ```python 43 | from webpack_manifest import webpack_manifest 44 | ``` 45 | 46 | If you copied the source file in, import it with 47 | 48 | ```python 49 | import webpack_manifest 50 | ``` 51 | 52 | Once you've imported the manifest loader... 53 | 54 | ```python 55 | manifest = webpack_manifest.load( 56 | # An absolute path to a manifest file 57 | path='/abs/path/to/manifest.json', 58 | 59 | # The root url that your static assets are served from 60 | static_url='/static/', 61 | 62 | # optional args... 63 | # ---------------- 64 | 65 | # Ensures that the manifest is flushed every time you call `load(...)` 66 | # If webpack is currently building, it will also delay until it's ready. 67 | # You'll want to set this to True in your development environment 68 | debug=False, 69 | 70 | # Max timeout (in seconds) that the loader will wait while webpack is building. 71 | # This setting is only used when the `debug` argument is True 72 | timeout=60, 73 | 74 | # If a manifest read fails during deserialization, a second attempt will be 75 | # made after a small delay. By default, if `read_retry` is `None` and `debug` 76 | # is `True`, it well be set to `1` 77 | read_retry=None, 78 | 79 | # If you want to access the actual file content, provide the build directory root 80 | static_root='/var/www/static/', 81 | ) 82 | 83 | # `load` returns a manifest object with properties that match the names of 84 | # the entries in your webpack config. The properties matching your entries 85 | # have `js` and `css` properties that are pre-rendered strings that point 86 | # to all your JS and CSS assets. Additionally, access internal entry data with: 87 | # `js.rel_urls` and `css.rel_urls` - relative urls 88 | # `js.content` and `css.content` - raw string content 89 | # `js.inline` and `css.inline` - pre-rendered inline asset elements 90 | 91 | # A string containing pre-rendered script elements for the "main" entry 92 | manifest.main.js # '' 108 | 109 | # A string containing pre-rendered inline style elements for the "main" entry 110 | manifest.main.css.inline # '' 111 | 112 | # Note: If you don't name your entry, webpack will automatically name it "main". 113 | ``` 114 | 115 | 116 | Usage in Django 117 | --------------- 118 | 119 | 120 | ```python 121 | INSTALLED_APPS = ( 122 | # ... 123 | 'webpack_manifest', 124 | ) 125 | 126 | WEBPACK_MANIFEST = { 127 | 'manifests': { 128 | 'my_project': { 129 | 'path': '/path/to/manifest.json', 130 | 'static_url': STATIC_URL, 131 | 'static_root': STATIC_ROOT, 132 | 'debug': DEBUG, 133 | }, 134 | }, 135 | } 136 | ``` 137 | 138 | ```html 139 | {% load webpack_manifest_tags %} 140 | 141 | {% load_webpack_manifest 'my_project' as manifest %} 142 | 143 | 144 | {{ manifest.main.css|safe }} 145 | 146 | 147 | {{ manifest.main.js|safe }} 148 | ``` 149 | 150 | 151 | How to run the tests 152 | -------------------- 153 | 154 | ``` 155 | pip install -r requirements.txt 156 | nosetests 157 | ``` 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "webpack-yam-plugin": "^0.4.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.7 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from webpack_manifest import webpack_manifest 3 | 4 | setup( 5 | name='webpack-manifest', 6 | version=webpack_manifest.__version__, 7 | packages=['webpack_manifest', 'webpack_manifest.templatetags'], 8 | description='Manifest loader that allows you to include references to files built by webpack', 9 | long_description='Documentation at https://github.com/markfinger/python-webpack-manifest', 10 | author='Mark Finger', 11 | author_email='markfinger@gmail.com', 12 | url='https://github.com/markfinger/python-webpack-manifest', 13 | ) 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/python-webpack-manifest/bb10dbb718f2b41d8356c983b375b064e220d521/tests/__init__.py -------------------------------------------------------------------------------- /tests/bar/woz.js: -------------------------------------------------------------------------------- 1 | bar_woz=1 2 | -------------------------------------------------------------------------------- /tests/foo/bar.css: -------------------------------------------------------------------------------- 1 | .foo_bar {} 2 | -------------------------------------------------------------------------------- /tests/foo/bar.js: -------------------------------------------------------------------------------- 1 | foo_bar=1 2 | -------------------------------------------------------------------------------- /tests/test_manifest_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "built", 3 | "errors": null, 4 | "files": { 5 | "main": [ 6 | "foo/bar.js", 7 | "woz/bar.css", 8 | "bar/woz.png" 9 | ], 10 | "foo": [ 11 | "foo/bar.js", 12 | "woz/bar.js", 13 | "bar/woz.js" 14 | ], 15 | "bar": [ 16 | "foo/bar.css", 17 | "woz/bar.css", 18 | "bar/woz.js" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /tests/test_manifest_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "errors", 3 | "errors": [ 4 | "error 1", 5 | "error 2" 6 | ], 7 | "files": null 8 | } -------------------------------------------------------------------------------- /tests/test_manifest_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "building", 3 | "errors": null, 4 | "files": null 5 | } -------------------------------------------------------------------------------- /tests/test_manifest_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "unknown status", 3 | "errors": null, 4 | "files": null 5 | } -------------------------------------------------------------------------------- /tests/test_webpack_manifest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from webpack_manifest import webpack_manifest 4 | 5 | TEST_ROOT = os.path.dirname(__file__) 6 | 7 | 8 | class TestBundles(unittest.TestCase): 9 | def test_raises_exception_for_missing_manifest(self): 10 | self.assertRaises( 11 | webpack_manifest.WebpackManifestFileError, 12 | webpack_manifest.read, 13 | '/path/that/does/not/exist', 14 | None, 15 | ) 16 | 17 | def test_manifest_entry_object_string_conversion(self): 18 | manifest = webpack_manifest.load( 19 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 20 | static_url='/static/', 21 | ) 22 | self.assertEqual(str(manifest.main.js), manifest.main.js.output) 23 | self.assertEqual(str(manifest.main.css), manifest.main.css.output) 24 | 25 | def test_manifest_provide_rendered_elements(self): 26 | manifest = webpack_manifest.load( 27 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 28 | static_url='/static/', 29 | ) 30 | self.assertEqual(manifest.main.js.output, '') 31 | self.assertEqual(manifest.main.css.output, '') 32 | 33 | self.assertEqual( 34 | manifest.foo.js.output, 35 | ( 36 | '' 37 | '' 38 | '' 39 | ) 40 | ) 41 | self.assertEqual(manifest.foo.css.output, '') 42 | 43 | self.assertEqual(manifest.bar.js.output, '') 44 | self.assertEqual( 45 | manifest.bar.css.output, 46 | ( 47 | '' 48 | '' 49 | ) 50 | ) 51 | 52 | def test_non_trailing_slash_static_url_handled(self): 53 | manifest = webpack_manifest.load( 54 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 55 | static_url='/static', 56 | ) 57 | self.assertEqual(manifest.main.js.output, '') 58 | self.assertEqual(manifest.main.css.output, '') 59 | 60 | def test_rel_urls(self): 61 | manifest = webpack_manifest.load( 62 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 63 | static_url='/static/', 64 | static_root=os.path.dirname(__file__), 65 | ) 66 | self.assertEqual(manifest.foo.js.rel_urls, ['foo/bar.js', 'woz/bar.js', 'bar/woz.js']) 67 | self.assertEqual(manifest.foo.css.rel_urls, []) 68 | self.assertEqual(manifest.bar.css.rel_urls, ['foo/bar.css', 'woz/bar.css']) 69 | 70 | def test_legacy_rel_urls(self): 71 | manifest = webpack_manifest.load( 72 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 73 | static_url='/static/', 74 | static_root=os.path.dirname(__file__), 75 | ) 76 | self.assertEqual(manifest.foo.rel_js, ['foo/bar.js', 'woz/bar.js', 'bar/woz.js']) 77 | self.assertEqual(manifest.bar.rel_css, ['foo/bar.css', 'woz/bar.css']) 78 | 79 | def test_missing_static_root_handled(self): 80 | try: 81 | manifest = webpack_manifest.load( 82 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 83 | static_url='/static/', 84 | debug=True, 85 | ) 86 | manifest.main.js.content 87 | self.assertFalse('should not reach this') 88 | except webpack_manifest.WebpackManifestConfigError as e: 89 | self.assertEqual( 90 | e.args[0], 91 | 'Provide static_root to access webpack entry content.', 92 | ) 93 | 94 | def test_content_output(self): 95 | manifest = webpack_manifest.load( 96 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 97 | static_url='/static/', 98 | static_root=os.path.dirname(__file__), 99 | debug=True, 100 | ) 101 | self.assertEqual(manifest.foo.js.content, 'foo_bar=1\n\nwoz_bar=1\n\nbar_woz=1\n') 102 | self.assertEqual(manifest.foo.css.content, '') 103 | self.assertEqual(manifest.bar.css.content, '.foo_bar {}\n\n.woz_bar {}\n') 104 | 105 | def test_content_inline(self): 106 | manifest = webpack_manifest.load( 107 | os.path.join(TEST_ROOT, 'test_manifest_1.json'), 108 | static_url='/static/', 109 | static_root=os.path.dirname(__file__), 110 | debug=True, 111 | ) 112 | self.assertEqual(manifest.foo.js.inline, '') 113 | self.assertEqual(manifest.foo.css.inline, '') 114 | self.assertEqual(manifest.bar.css.inline, '') 115 | 116 | def test_errors_handled(self): 117 | try: 118 | webpack_manifest.load( 119 | os.path.join(TEST_ROOT, 'test_manifest_2.json'), 120 | static_url='/static', 121 | ) 122 | self.assertFalse('should not reach this') 123 | except webpack_manifest.WebpackError as e: 124 | self.assertEqual( 125 | e.args[0], 126 | 'Webpack errors: \n\nerror 1\n\nerror 2', 127 | ) 128 | 129 | def test_status_handled(self): 130 | try: 131 | webpack_manifest.load( 132 | os.path.join(TEST_ROOT, 'test_manifest_2.json'), 133 | static_url='/static', 134 | ) 135 | self.assertFalse('should not reach this') 136 | except webpack_manifest.WebpackError as e: 137 | self.assertEqual( 138 | e.args[0], 139 | 'Webpack errors: \n\nerror 1\n\nerror 2', 140 | ) 141 | 142 | def test_handles_timeouts_in_debug(self): 143 | path = os.path.join(TEST_ROOT, 'test_manifest_3.json') 144 | try: 145 | webpack_manifest.load( 146 | path, 147 | static_url='/static', 148 | debug=True, 149 | timeout=1, 150 | ) 151 | self.assertFalse('should not reach this') 152 | except webpack_manifest.WebpackManifestBuildingStatusTimeout as e: 153 | self.assertEqual( 154 | e.args[0], 155 | 'Timed out reading the webpack manifest at "{}"'.format(path), 156 | ) 157 | 158 | def test_handles_unknown_statuses(self): 159 | path = os.path.join(TEST_ROOT, 'test_manifest_4.json') 160 | try: 161 | webpack_manifest.load( 162 | path, 163 | static_url='/static', 164 | ) 165 | self.assertFalse('should not reach this') 166 | except webpack_manifest.WebpackManifestStatusError as e: 167 | self.assertEqual( 168 | e.args[0], 169 | 'Unknown webpack manifest status: "unknown status"', 170 | ) 171 | 172 | def test_handles_missing_entries(self): 173 | path = os.path.join(TEST_ROOT, 'test_manifest_1.json') 174 | try: 175 | manifest = webpack_manifest.load( 176 | path, 177 | static_url='/static', 178 | ) 179 | manifest.glub 180 | self.assertFalse('should not reach this') 181 | except webpack_manifest.WebpackErrorUnknownEntryError as e: 182 | self.assertEqual( 183 | e.args[0], 184 | 'Unknown entry "glub" in manifest "%s"' % path, 185 | ) 186 | -------------------------------------------------------------------------------- /tests/woz/bar.css: -------------------------------------------------------------------------------- 1 | .woz_bar {} 2 | -------------------------------------------------------------------------------- /tests/woz/bar.js: -------------------------------------------------------------------------------- 1 | woz_bar=1 2 | -------------------------------------------------------------------------------- /webpack_manifest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/python-webpack-manifest/bb10dbb718f2b41d8356c983b375b064e220d521/webpack_manifest/__init__.py -------------------------------------------------------------------------------- /webpack_manifest/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/python-webpack-manifest/bb10dbb718f2b41d8356c983b375b064e220d521/webpack_manifest/templatetags/__init__.py -------------------------------------------------------------------------------- /webpack_manifest/templatetags/webpack_manifest_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from webpack_manifest import webpack_manifest 4 | 5 | if not hasattr(settings, 'WEBPACK_MANIFEST'): 6 | raise webpack_manifest.WebpackManifestConfigError('`WEBPACK_MANIFEST` has not been defined in settings') 7 | 8 | if 'manifests' not in settings.WEBPACK_MANIFEST: 9 | raise webpack_manifest.WebpackManifestConfigError( 10 | '`WEBPACK_MANIFEST[\'manifests\']` has not been defined in settings' 11 | ) 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.simple_tag 17 | def load_webpack_manifest(name): 18 | if name not in settings.WEBPACK_MANIFEST['manifests']: 19 | raise webpack_manifest.WebpackManifestConfigError( 20 | '"%s" has not been defined in `WEBPACK_MANIFEST[\'manifests\']`' % name, 21 | ) 22 | 23 | conf = settings.WEBPACK_MANIFEST['manifests'][name] 24 | 25 | for prop in ('path', 'static_url', 'static_root'): 26 | if prop not in conf: 27 | raise webpack_manifest.WebpackManifestConfigError( 28 | '"%s" has not been defined in `WEBPACK_MANIFEST[\'manifests\'][\'%s\']`' % (prop, name), 29 | ) 30 | 31 | return webpack_manifest.load(**conf) 32 | -------------------------------------------------------------------------------- /webpack_manifest/webpack_manifest.py: -------------------------------------------------------------------------------- 1 | """ 2 | webpack_manifest.py - https://github.com/markfinger/python-webpack-manifest 3 | 4 | MIT License 5 | 6 | Copyright (c) 2015-present, Mark Finger 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | import os 28 | import json 29 | import time 30 | from datetime import datetime, timedelta 31 | 32 | __version__ = '2.1.1' 33 | 34 | MANIFEST_CACHE = {} 35 | 36 | BUILDING_STATUS = 'building' 37 | BUILT_STATUS = 'built' 38 | ERRORS_STATUS = 'errors' 39 | 40 | 41 | def load(path, static_url, debug=False, timeout=60, read_retry=None, static_root=None): 42 | # Enable failed reads to be retried after a delay of 1 second 43 | if debug and read_retry is None: 44 | read_retry = 1 45 | 46 | if debug or path not in MANIFEST_CACHE: 47 | manifest = build(path, static_url, debug, timeout, read_retry, static_root) 48 | 49 | if not debug: 50 | MANIFEST_CACHE[path] = manifest 51 | 52 | return manifest 53 | 54 | return MANIFEST_CACHE[path] 55 | 56 | 57 | def build(path, static_url, debug, timeout, read_retry, static_root): 58 | data = read(path, read_retry) 59 | status = data.get('status', None) 60 | 61 | if debug: 62 | # Lock up the process and wait for webpack to finish building 63 | max_timeout = datetime.utcnow() + timedelta(seconds=timeout) 64 | while status == BUILDING_STATUS: 65 | time.sleep(0.1) 66 | if datetime.utcnow() > max_timeout: 67 | raise WebpackManifestBuildingStatusTimeout( 68 | 'Timed out reading the webpack manifest at "{}"'.format(path) 69 | ) 70 | data = read(path, read_retry) 71 | status = data.get('status', None) 72 | 73 | if status == ERRORS_STATUS: 74 | raise WebpackError( 75 | 'Webpack errors: \n\n{}'.format( 76 | '\n\n'.join(data['errors']) 77 | ) 78 | ) 79 | 80 | if status != BUILT_STATUS: 81 | raise WebpackManifestStatusError('Unknown webpack manifest status: "{}"'.format(status)) 82 | 83 | return WebpackManifest(path, data, static_url, static_root) 84 | 85 | 86 | class WebpackManifest(object): 87 | def __init__(self, path, data, static_url, static_root=None): 88 | self._path = path 89 | self._data = data 90 | self._files = data['files'] 91 | self._static_url = static_url 92 | self._static_root = static_root 93 | self._manifest_entries = {} 94 | 95 | def __getattr__(self, item): 96 | if item in self._manifest_entries: 97 | return self._manifest_entries[item] 98 | 99 | if item in self._files: 100 | manifest_entry = WebpackManifestEntry(self._files[item], self._static_url, self._static_root) 101 | self._manifest_entries[item] = manifest_entry 102 | return manifest_entry 103 | 104 | raise WebpackErrorUnknownEntryError('Unknown entry "%s" in manifest "%s"' % (item, self._path)) 105 | 106 | 107 | class WebpackManifestTypeEntry(object): 108 | def __init__(self, manifest, static_url, static_root=None): 109 | self.manifest = manifest 110 | self.static_url = static_url 111 | self.static_root = static_root 112 | 113 | self.rel_urls = [] 114 | self.output = '' 115 | self._content = None 116 | if self.static_root: 117 | self.paths = [] 118 | 119 | def add_file(self, rel_path): 120 | rel_url = '/'.join(rel_path.split(os.path.sep)) 121 | self.rel_urls.append(rel_url) 122 | self.output += self.template.format(self.static_url + rel_url) 123 | if self.static_root: 124 | self.paths.append(os.path.join(self.static_root, rel_path)) 125 | 126 | def __str__(self): 127 | return self.output 128 | 129 | @property 130 | def content(self): 131 | if self._content is None: 132 | if not self.static_root: 133 | raise WebpackManifestConfigError("Provide static_root to access webpack entry content.") 134 | 135 | buffer = [] 136 | for path in self.paths: 137 | with open(path, 'r') as content_file: 138 | buffer.append(content_file.read()) 139 | self._content = '\n'.join(buffer) 140 | return self._content 141 | 142 | @property 143 | def inline(self): 144 | content = self.content 145 | return self.inline_template.format(content) if content else '' 146 | 147 | 148 | class WebpackManifestJsEntry(WebpackManifestTypeEntry): 149 | template = '' 150 | inline_template = '' 151 | 152 | 153 | class WebpackManifestCssEntry(WebpackManifestTypeEntry): 154 | template = '' 155 | inline_template = '' 156 | 157 | 158 | class WebpackManifestEntry(object): 159 | supported_extensions = { 160 | 'js': WebpackManifestJsEntry, 161 | 'css': WebpackManifestCssEntry, 162 | } 163 | 164 | def __init__(self, rel_paths, static_url, static_root=None): 165 | # Frameworks tend to be inconsistent about what they 166 | # allow with regards to static urls 167 | if not static_url.endswith('/'): 168 | static_url += '/' 169 | 170 | self.rel_paths = rel_paths 171 | self.static_url = static_url 172 | self.static_root = static_root 173 | 174 | for ext, ext_class in self.supported_extensions.items(): 175 | setattr(self, ext, ext_class(self, static_url, static_root)) 176 | 177 | # Build strings of elements that can be dumped into a template 178 | for rel_path in rel_paths: 179 | name, ext = os.path.splitext(rel_path) 180 | ext = ext.lstrip('.').lower() 181 | if ext in self.supported_extensions: 182 | getattr(self, ext).add_file(rel_path) 183 | 184 | # Backwards compatibility accessors 185 | 186 | @property 187 | def rel_js(self): 188 | return self.js.rel_urls 189 | 190 | @property 191 | def rel_css(self): 192 | return self.css.rel_urls 193 | 194 | 195 | def read(path, read_retry): 196 | if not os.path.isfile(path): 197 | raise WebpackManifestFileError('Path "{}" is not a file or does not exist'.format(path)) 198 | 199 | if not os.path.isabs(path): 200 | raise WebpackManifestFileError('Path "{}" is not an absolute path to a file'.format(path)) 201 | 202 | with open(path, 'r') as manifest_file: 203 | content = manifest_file.read() 204 | 205 | # In certain conditions, the file's contents evaluate to an empty string, so 206 | # we provide a hook to perform a single retry after a delay. 207 | # While it's a difficult bug to pin down it can happen most commonly during 208 | # periods of high cpu-load, so the suspicion is that it's down to race conditions 209 | # that are a combination of delays in the OS writing buffers and the fact that we 210 | # are handling two competing processes 211 | try: 212 | return json.loads(content) 213 | except ValueError: 214 | if not read_retry: 215 | raise 216 | 217 | time.sleep(read_retry) 218 | return read(path, 0) 219 | 220 | 221 | class WebpackManifestFileError(Exception): 222 | pass 223 | 224 | 225 | class WebpackError(Exception): 226 | pass 227 | 228 | 229 | class WebpackManifestStatusError(Exception): 230 | pass 231 | 232 | 233 | class WebpackManifestBuildingStatusTimeout(Exception): 234 | pass 235 | 236 | 237 | class WebpackErrorUnknownEntryError(Exception): 238 | pass 239 | 240 | 241 | class WebpackManifestConfigError(Exception): 242 | pass 243 | --------------------------------------------------------------------------------