Small bridge to make Flask work with webpack manifests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This little module integrates Flask and webpack for a nice developer experience. | |
To make this work with your Flask app, import this in app.py: | |
from lib.webpack import Webpack | |
And then make sure to run this code during app initialization in app.py: | |
webpack = Webpack() | |
webpack.init_app(app) | |
WEBPACK_MANIFEST_PATH = './static/gen/manifest.json' | |
Then, in your webpack configuration, make sure to use the `webpack-manifest-plugin`, | |
which will generate a manifest file (JSON file of all build artifacts) on every webpack build. | |
That's the file that this module parses at both development-time and build-time to | |
include concrete webpack-built artifacts in your project. | |
In `webpack.config.json`, you need something like this: | |
const WebpackManifestPlugin = require('webpack-manifest-plugin'); | |
// ... | |
plugins: [ | |
// ... | |
// generates ./static/gen/manifest.json | |
new WebpackManifestPlugin() | |
// ... | |
] | |
// ... | |
Once you make use of this module, you need to run webpack and the Flask development server | |
side-by-side. In `package.json`, I'd then spin up Flask & webpack dev servers concurrently | |
using this configuration: | |
"scripts": { | |
"jsclean": "rm static/gen/*", | |
"jsbuild": "webpack --mode=production --progress", | |
"jsserver": "webpack --mode=development --progress --watch", | |
"pybuild": "python app.py build", | |
"pyserver": "python app.py runserver", | |
"build": "npm-run-all jsbuild pybuild", | |
"start": "concurrently -n \"js,py\" -c \"bgBlue.bold,bgGreen.bold\" npm:jsserver npm:pyserver", | |
} | |
Where `npm build` is using Frozen-Flask to create static HTML and using webpack to generate static JS. | |
Meanwhile, `npm start` is using `jsserver` and `pyserver` to run local webpack and Flask servers | |
side-by-side. The tool `concurrently` just makes it so that the log output is nice. | |
Every time JS files changed, a new webpack automatic build generates a new webpack manifest, | |
and the Flask development server uses that when re-rendering Jinja templates, using the | |
freshly-built JavaScript artifacts. This updates the URLs used by `asset_url_for(...)`, | |
the registered function in the Jinja context provided by this module, which ensures | |
freshly-built assets load in your browser. It all wires together quite cleanly. | |
For more on the "theory" behind this module and how it brings a Modern JavaScript developer experience | |
to a simple Python web framework like Flask, read [JavaScript: The Modern Parts][js-modern-parts]. | |
[js-modern-parts]: https://amontalenti.com/2019/08/10/javascript-the-modern-parts | |
""" | |
import json | |
import os | |
from flask import current_app | |
class Webpack(object): | |
def __init__(self, app=None): | |
self.app = app | |
self.last_mtime = 0 | |
if app is not None: | |
self.init_app(app) | |
def init_app(self, app): | |
""" | |
Mutate the application passed in as explained here: | |
http://flask.pocoo.org/docs/0.10/extensiondev/ | |
:param app: Flask application | |
:return: None | |
""" | |
# Setup a few sane defaults. | |
app.config.setdefault('WEBPACK_MANIFEST_PATH', | |
'/tmp/WEBPACK_MANIFEST_PATH__BAD_PATH') | |
app.config.setdefault('WEBPACK_ASSETS_URL', None) | |
app.before_request(self._refresh_webpack_manifest) | |
if hasattr(app, 'add_template_global'): | |
app.add_template_global(self.asset_url_for) | |
else: | |
# Flask < 0.10 | |
ctx = { | |
'asset_url_for': self.asset_url_for | |
} | |
app.context_processor(lambda: ctx) | |
def _set_asset_paths(self, app): | |
""" | |
Read in the manifest json file which acts as a manifest for assets. | |
This allows us to get the asset path as well as hashed names. | |
:param app: Flask application | |
:return: None | |
""" | |
webpack_manifest = app.config['WEBPACK_MANIFEST_PATH'] | |
try: | |
current_mtime = os.path.getmtime(webpack_manifest) | |
last_mtime = self.last_mtime | |
self.last_mtime = current_mtime | |
# don't reload file if it hasn't changed | |
if current_mtime <= last_mtime: | |
return | |
except OSError: | |
raise RuntimeError( | |
"Flash-Webpack failed to check mtime of webpack manifest") | |
try: | |
with app.open_resource(webpack_manifest, 'r') as manifest_json: | |
manifest_obj = json.load(manifest_json) | |
self.assets = manifest_obj | |
except IOError: | |
raise RuntimeError( | |
"Flask-Webpack requires 'WEBPACK_MANIFEST_PATH' to be set and " | |
"it must point to a valid json file.") | |
def _refresh_webpack_manifest(self): | |
""" | |
Refresh the webpack stats so we get the latest version. It's a good | |
idea to only use this in development mode. | |
:return: None | |
""" | |
self._set_asset_paths(current_app) | |
def asset_url_for(self, asset): | |
""" | |
Lookup the hashed asset path of a file name unless it starts with | |
something that resembles a web address, then take it as is. | |
:param asset: A logical path to an asset | |
:type asset: str | |
:return: Asset path or None if not found | |
""" | |
if '//' in asset: | |
return asset | |
if asset not in self.assets: | |
raise NameError("Asset not found: " + asset) | |
return '{0}'.format(self.assets[asset]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment