Skip to content

Instantly share code, notes, and snippets.

@amontalenti
Last active Aug 16, 2020
Embed
What would you like to do?
Small bridge to make Flask work with webpack manifests
"""
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