Skip to content

Instantly share code, notes, and snippets.

@amontalenti
Last active August 16, 2020 00:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amontalenti/ffeca0dce10f29d42a82e80773804355 to your computer and use it in GitHub Desktop.
Save amontalenti/ffeca0dce10f29d42a82e80773804355 to your computer and use it in GitHub Desktop.
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