├── .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 |
--------------------------------------------------------------------------------