Flask-Rollup

Rollup Javascript bundler integration with Flask web framework. Because we need modern tools for modern web development.

While development of SPA is pretty straightforward since there is strict separation of backend and frontend code, the development of Javascript-rich Multi-Page Applications requires great deal of extra code that needs to be put to properly process Javascript and produce appropriately bundled output. While it is possible to go with pre-bundled dependencies, such approach limits the ability to debug and analyse program execution, since usually these bundles are already minified and rarely provide source maps. This is where bundlers come in handy, these programs can output either non-minified bundles for development and minified for production, provide tree shaking feature for unused code removal and can limit included code to only what’s used on particular page (code splitting).

There are many Javascript bundlers out there, providing different feature sets and with different goals. The idea of Flask-Rollup came from a specific set of requirements:

  • ES6 modules as the output of the bundling process

  • tree shaking done well

  • extensive command line operation supported by configuration loaded from file

  • can process CommonJS dependencies to include code in form that’s compatible with output

Rollup meets all these requirements. It’s possible others do that too, but for now let’s focus on Rollup.

The goal is to integrate Rollup as Javascript bundler with Flask application development process as seamlessly as it is possible. In most basic case this should not require any interaction with npm/npx but instead provide familiar interface of Flask CLI, extended to support Rollup commands and process.

Dependencies

NodeJS 10 (with npm), Python 3.7 and Flask 1.1. We’re modern.

Installation

Use pip to install officially released version from PyPI.

$ pip install -U flask-rollup

It does not matter how NodeJS has been installed, it just needs to be available in system search path along with npm. If unsure use either system provided ones (Ubuntu 20.04: NodeJS 10.19.0 and npm 6.14.4) or use nvm tool to install locally any arbitrary version.

Basic usage

After installing Flask-Rollup in Python virtual environment an environment for Rollup has to be initialised. This extension adds CLI command group rollup and one of provided commands initialise all Javascript tooling for Rollup.

$ flask rollup --help
Usage: flask rollup [OPTIONS] COMMAND [ARGS]...

  Rollup Javascript bundler commands

Options:
  --help  Show this message and exit.

Commands:
  init  Initialise Rollup environment
  run   Run Rollup and generate all registered bundles

Running flask rollup init will create bare bones Javascript project control file package.json, install Rollup and all required plugins and finally create generic Rollup configuration file in rollup.config.js. All these artifacts are generated in current working directory so these commands may be safely tested outside of application code tree.

init command takes optional flag --babel which signals if Babel transpiler should also be installed along with related plugins. This is important if you want your Javascript code to be transpiled down to ES6 and allows you to write it using features from any newer or experimental ES version, like spread operator for object literals from ES9 that’s still marked as experimental with CanIUse. With this option enabled, a bare bones Babel configuration file will be written as babel.config.json. This configuration will include only options related to transpilation to target ES version so you can freely modify it to include other Babel options.

Once initialisation is done, the extension does not modify anything in Javascript environment so all updates to packages have to be processed the Javascript way (eg. with npm i -D rollup-plugin-something-fancy and then adding it to Rollup pipeline in rollup.config.js).

With Javascript environment ready Rollup can start bundling Javascript code of the application. Internally definition expressed as instance of Bundle is translated into series of Rollup command line params. Simplest bundle definition can look like the below code.

from flask_rollup import Bundle

bundle = Bundle(
    name='auth.login', target_dir='dist/js',
    entrypoints=['js/auth.login.js']
)

Both entrypoint and target paths are relative to application static folder. The above definition will produce ES6 module dist/js/auth.login.[hash].js and source map file dist/js/auth.login.[hash].js.map. The module will include all code that was imported from installed modules thanks to preconfigured plugin that resolves imports from NodeJS location (node_modules directory). In production mode the bundle code will also be minified with Terser.

If entrypoints Javascript code depends on any other module that’s not installed in node_modules, it should be listed in bundle’s dependencies list. Rollup bundles this code without any issues, but in Python the module content is not parsed so all such dependencies have to be specified manually so the bundle gets rebuilt once they change. This is important only in development mode when bundles are automatically rebuilt upon code changes.

from flask_rollup import Bundle

bundle = Bundle(
    name='auth.login', target_dir='dist/js',
    entrypoints=['js/auth.login.js'],
    dependencies=['js/utils.js', 'js/security.js'],
)

Once bundle is registered it may be generated with flask rollup run. For convenience in development mode bundles are built automatically if there are any changes to its entrypoints or dependencies.

Production vs development mode

The operation of Rollup with regards to environment is controlled by Flask environment variable FLASK_ENV. It’s automatically set by Flask but may be also controlled with startup scripts or python-dotenv package. This variable is directly translated to NODE_ENV and to process.environment in Javascript code in consequence.

Extension configuration

This extension uses following configuration options.

ROLLUP_PATH

path to rollup executable, if not provided it will be assumed it’s available in system search path as rollup

ROLLUP_CONFIG_JS

path to rollup.config.js file with Rollup configuration, it has to be provided for running web application and may be omitted for CLI operations, it will be assumed this file is present in current working directory; this must be set when in production mode

Rollup bundling configuration

Initialisation function produces generic Rollup config file rollup.config.js which in most cases is sufficient but may be modified to specific needs. Since the extension controls Rollup command line, the only way to change Rollup behaviour is with this configuration file. The distinction between values coming from Rollup configuration and command line should be kept strict and resolved as follows:

  • command line options tell Rollup what to do (what input files to process)

  • configuration tells Rollup how to do it (how to process input files)

Note

Modifications to rollup.config.js should take into consideration how Rollup processes configuration and command line - the options are not overwritten but merged instead. Including bundle parameters like entrypoints or paths in rollup.config.js (iow what to do) may produce undesirable side effects.

The what to do part is controlled by bundle definitions in your code.

Be aware that processing additional (non-Javascript) files is neither natively supported by Rollup or this extension. In other words, if you think you can make Rollup (or Flask-Rollup) to process your SCSS or PostCSS modules then you better stop. It’s not intended to do that. Use Flask-Assets.

Template function

This extension registers global template function jsbundle that takes bundle name as an argument and returns bundle url to be included in Jinja2 template. In particular, it can be used in Javascript code like below.

{% block scripts %}
<script type="module">
    import { html, render, Dashboard } from '{{ jsbundle(request.endpoint) }}';
    render(
        html`<${Dashboard} />`,
        document.getElementById('dashboard-block'),
    );
</script>
{% endblock %}

To make this work, bundles should be named after route endpoints where they are supposed to be included.

API Documentation

class flask_rollup.Rollup(app: Optional[Flask] = None)

Rollup integration with Flask. Extension can be registered in both simple way or with init_app(app) pattern.

init_app(app: Flask)

Initialise application. This function sets up required configuration defaults and initial Rollup command line args. In non-production mode autorebuild is enabled. Template function jsbundle is registered here as Jinja2 global object.

Parameters:

app – application object

register(bundle: Bundle)

Register bundle. At this moment input paths are resolved. If any output matching file is present, the bundle output is resolved with short circuit, generated otherwise.

Parameters:

bundle – bundle object to be registered

run_rollup(bundle_name: str)

Run Rollup bundler over specified bundle if bundle state changed. Once Rollup finishes bundle’s output is resolved (paths and url).

Parameters:

bundle_name – name of the bundle to be rebuilt

class flask_rollup.Bundle(name: str, target_dir: str, entrypoints: ~typing.List[~typing.Union[~flask_rollup.Entrypoint, str]], dependencies: ~typing.List[str] = <factory>)

Javascript bundle definition. Required arguments are name, target_dir and entrypoints. If any of entrypoints has a dependency on non-installed module, it should be listed in dependencies. These modules are used to calculate state of the bundle and failing to include them may result in bundle not being rebuilt if they change.

Bundles should be named after Flask app endpoints for effective code splitting, eg. Javascript module used on page auth.login should be placed in a bundle named auth.login. Upon bundling the result will be in file target_dir/auth.login.[hash].js, and corresponding source map in file target_dir/auth.login.[hash].js.map. This is required to use jsbundle in templates and to have working auto rebuild in development mode.

In simplest case of single entrypoint it may be specified as string denoting path relative to app static folder. In any case bundle can have only one unnamed entrypoint and this condition is validated early in the process.

Parameters:
  • name – name of the bundle

  • target_dir – where the output will be stored, relative to static directory root

  • entrypoints – list of bundle entrypoints

  • dependencies – list of entrypoint’s dependencies that will be included in bundle

Raises:

BundleDefinitionError – if definition contains more than 1 unnamed entrypoint

argv() List[str]

Return list of Rollup command line params required to build the bundle.

Returns:

list of command line param tokens

Return type:

List[str]

calc_state() str

Calculate bundle state checksum. This is used to determine if bundle should be rebuilt in development mode. For each input path (entrypoints and dependencies) file modification time is used as a base of calculation.

Returns:

bundle state checksum

Return type:

str

clean_artifacts()

Delete bundle artifacts (Javascript and maps).

resolve_output(root: str, url_path: str)

Determine bundle’s generation output paths (both absolute file system path and relative to static folder) and url.

Parameters:
  • root – static content root directory (application static folder)

  • url_path – path to static content

resolve_paths(root: str)

Make bundle paths absolute and normalized.

Parameters:

root – root path (application static folder path)

class flask_rollup.Entrypoint(path: str, name: str = '')

Entrypoint information: path and name.

Parameters:
  • path – entrypoint path

  • name – name of entrypoint, defaults to empty string

cmdline_param() str

Generate command line param from name and path.

Returns:

Rollup command line param

Return type:

str

Advanced usage patterns

Local Javascript code dependencies

If Javascript code uses local dependencies (eg imported from local modules, as opposed to installed libraries), Rollup will properly pick up modifications to both entrypoint and to imported code. Unfortunately Flask-Rollup does not analyse Javascript code and has to be provided with static list of local dependencies to be able to determine state of bundle while in development mode (whether it’s dirty and needs to be regenerated or did not change). Bundle takes dependencies argument which is a list of paths (still - relative to static directory) to be considered a dependency when calculating bundle state.

Multiple entrypoints

Specify multiple entrypoints to get chunked output. This is not always usable for code splitting (which with the above mentioned convention of naming bundles after Flask view endpoints may be easily implemented on Python side) but for example to conditionally include some debug code. If the bundle should produce chunked output, entrypoints param to Bundle constructor can include more elements. These elements may be Entrypoint instances or plain strings but the rule is that only one of them may be unnamed (string entrypoint elements are unnamed by its nature). Generated chunks will have names of respective entrypoints.

Indices and tables