flask-assets-pipeline 0.8.0
Modern asset pipeline for Flask
Requires Python: >=3.10

# Flask-Assets-Pipeline

Modern assets pipeline for Flask.

 - Asset bundling using [esbuild](https://esbuild.github.io/)
 - Live reloading
 - Support for [tailwind](https://tailwindcss.com/) (v4)
 - Compatible with #nobuild leveraging [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
 - Extract inline scripts and styles from templates and bundle them properly
 - CDN support
 - Images optimization and resizing
 - Preloading and prefetching support
 - Generate a cache service worker

## Installation

Using pip:

    pip install flask-assets-pipeline

You will need to install esbuild:

    npm install esbuild

## Usage

### Introduction

Initialize the AssetsPipeline extension and list your JS and CSS that need bundling, located in the app's static directory:

```python
from flask import Flask
from flask_assets_pipeline import AssetsPipeline

app = Flask(__name__)
assets = AssetsPipeline(app, bundles=['app.js'])
```

> [!INFO]
> Related stylesheets will be automatically included when their corresponding js is included.

In your template, use the `asset_tags` directive to render the needed tags to include your assets.

```jinja
<head>
    {% asset_tags %}
</head>
```

### Development mode

While in development, use the `flask assets dev` command to launch esbuild (and optionnaly tailwind). This will watch your files and expose a live reloading endpoint.
Source maps are automatically enabled in development mode.

### Bundles

Bundles are the files that will be used as inputs in the esbuild command.  
Filenames are relative to the assets folder unless it's an absolute path. Bundles can also contain URLs that will be ignored during the build process but that will be used in includes.

Bundles can either be a list of files or a dict of named bundles:

```python
assets = AssetsPipeline(app, bundles={
    "@default": ['base.js'],
    "@home": ['home.js', 'home.css']
}, include=["@default"])

@app.route("/")
def home():
    assets.include("@home")
    return render_template("home.html")
```

When using named bundles, its files are not concatanated together. It is simply a way to reference multiple files at once.
Files from all named bundles will be provided as input in the esbuild command.

> [!NOTE]
> Named bundles do not need to start their name with @ but it's a good convention to follow.

[Code splitting](https://esbuild.github.io/api/#splitting) is enabled by default.

The `bundle()` and `blueprint_bundle()` methods allow you to register bundles with more options.

### Manage included assets

By default, all bundled assets are included in your pages. You can change this behavior and manually select what asset to include on a given page.

```python
assets = AssetsPipeline(app, bundles=[
    'base.js',
    'home.js'
], include=["base.js"])

@app.route("/")
def home():
    assets.include("home.js")
    return render_template("home.html")
```

The `include_asset()` function is also available in templates. It does not return anything, only includes assets for `{% asset_tags %}` to take into account.

In the include list, you can also add paths for any files in the static folder and urls.

```python
assets = AssetsPipeline(app, bundles={
    'base.js',
    'home.js'
}, include=[
    "base.js",
    "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
])
```

> [!WARNING]
> By default, included assets that are not bundles won't be included as modules (`<script type="module">`). Change that by prefixing them with "import ".
>
> ```python
> assets = AssetsPipeline(app, bundles={
>     "base.js"
> }, include=[
>     "base.js",
>     "import other.js"
> ])
> ```

### Including media assets

For assets other than scripts and stylesheets, use `asset_url(filename)` in your templates to get the url.

```jinja
<img src="{{ asset_url('cat.jpg') }}">
```

## Building for production

Use the following command to build assets for production:

    $ flask assets build

This will:

 - build your bundles using esbuild
 - build your css with tailwind if enabled
 - minify files and not produce source maps
 - optionally, copy static files from the assets folder to the static folder and stamp them
 - generate a mapping file (default: assets.json)

Files built using esbuild and tailwind will be generated in a dist folder in the static directory.

> [!IMPORTANT]
> Make sure to ship all generated files and the mapping file to production

> [!TIP]
> It is not recommended to build in production. You should build in a separate environment (eg: CI),
> then generate a tarball (or any other deliverable, eg: docker image) that you ship to production.

## Tailwind (v4) support

Flask-Assets-Pipeline can launch tailwind in the background at the same time as esbuild and build your css file.
The output css file will be automatically included.

You will need to install Tailwind CLI: `npm install tailwindcss @tailwindcss/cli`

```python
assets = AssetsPipeline(app, bundles=[
    'base.js'
], tailwind='main.css')
```

If the input file is missing, it will be automatically generated.
Make sure your tailwind input file lists the correct sources.

The extension can also automatically add sources and expand environment variables in your tailwind input file.
Set `ASSETS_TAILWIND_EXPAND_ENV_VARS` to `True` or configure `ASSETS_TAILWIND_SOURCES`.

## Using a separate assets folder

You can use a separate assets folder than your app static folder. This folder won't be exposed in production so raw assets are safe.

When using a separate assets folder:

 - Always use `asset_url()` for your assets' urls
 - In development, the assets folder will be exposed and assets served directly from it
 - For production, you must call the build command
 - During the build, assets which are not part of bundles will be copied to the static folder with a hash in their filename (for cache busting)

```python
assets = AssetsPipeline(app, bundles=['app.js'], assets_folder='assets')
```

## Inline assets

Flask-Assets-Pipeline allows to define scripts and styles directly in your templates. Their content will be extracted and bundled using the previously mentionned process.

To enable inline assets, set the inline option to true.

```python
assets = AssetsPipeline(app, ..., inline=True)
```

Add the `bundle` attribute to your script and style tags to extract and bundle them.

Example *datatable.html* template:

```jinja
<table class="datatable">
    <!-- ... -->
</table>

<script bundle>
document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll(".datatable").forEach(table => {
        // ...
    });
});
</script>

<style bundle>
.datatable {
    /* ... */
}
</style>
```

When this template is used as part of your request, its associated assets will be automatically included.

> [!IMPORTANT]
> No jinja directives can be used inside the bundled script and style tags

> [!NOTE]
> Extracted assets will be stored in your assets folder.  
> You can customize the name of the extracted files using a value for the bundle attribute: `<script bundle="filename.js">`

## Frontend routes

If your are building a Single Page App (SPA), you will use a frontend router. To make sure these routes exist on the backend,
you can register them:

```python
assets.add_route("login", "/login")
assets.add_route("account", "/account", decorators=[login_required]) # apply decorators

assets.add_catch_all_route() # catch all unknown paths
```

These routes simply return a rendered template defined using `ASSETS_ROUTE_TEMPLATE` (default is *frontend_route.html*)

## No build

If you're not planning to use any feature that require preprocessing (eg. typescript) or are using an externel build system, you can skip defining bundles.

```python
assets = AssetsPipeline(app, include=['app.js', 'app.css'])
```

> [!WARNING]
> Script mentionned in include are not included as module by default. If you wish to do so, prefix them with "import ":
>
> ```
> assets = AssetsPipeline(app, include=['import app.js', 'app.css'])
> ```

> [!WARNING]
> If you are using a separate assets folder, you will still need to build for production as assets need to
> be copied to the static folder

When using no build, it may be needed to edit the [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap):

```python
assets = AssetsPipeline(app, ..., import_map={"mypkg": "https://..."})
assets.map_import("myotherpkg", "https://...")
```

The import map will be rendered as part of `{% asset_tags %}`.

You may want to expose some package from your node_modules directory to import them in your scripts.
Exposed packages will be bundled using esbuild and added to the import map.

```python
assets = AssetsPipeline(app, ..., expose_node_packages=["mypkg"])
assets.expose_node_package("myotherpkg")
```

You can then import them as usual in your scripts.

## Preloading and prefetching

You can include assets and urls for [preloading](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload) and/or [prefetching](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/prefetch).

Similarly to how "import" can be specified, use "preload" in front of your included asset. The preload content type will be determined based on the file extension. You can specify it manually using `preload as TYPE URL`.

For prefetching, use "prefetch" as the prefix.

```python
assets = AssetsPipeline(app, include=["preload dependency.js", "preload as image cat.jpg", "prefetch page.js"])
```

## Including external assets

Any URLs can be used in the include list.

It is possible to provide additionnal metadata like crossorigin and integrity hash.

```python
assets = AssetsPipeline(app, include=["http://example.com/external.js#integrity=sha384-XXXX"])
```

Any parameter in the fragment part will be transformed to attributes of the html element used to include the asset.

By default, `crossorigin="anonymous"` is automatically applied to any url used.

## Using a CDN

Set the cdn host in your configuration to use a CDN when debug mode is not enabled.

```python
assets = AssetsPipeline(app, ..., cdn_host="https://xxx.cloudfront.net")
```

You can control when the cdn is used or not using the ASSETS_CDN_ENABLED config option (it will override the default behavior).

## Processing images

> [!WARNING]
> You must install pillow for this feature to be available

To activate image processing, you must either:

- use a separate assets folder
- define an images folder using `ASSETS_IMAGES_FOLDER` (relative to the static dir)

The extension can:

- save images dimensions as part of the mapping file
- convert images to webp
- optimize images (keeping their original format)
- resize images to various width for responsive images
- generate a blurry placeholder

Generate an image tag attributes using the metadata saved as part of the mapping file. Use `flask_assets_pipeline.image_attrs()` or `asset_image_attrs()` in templates.

```python
assets = AssetsPipeline(app, ..., assets_folder="assets", webp_images=True, resize_images=[400])
```

In your template:

```jinja
<img {{asset_image_attrs('image.png')}}>
```

Will result in:

```html
<img src="/static/dist/img/image.webp" width="800" height="600" srcset="/static/dist/img/image-400.webp 400w"
    style="background: url('data:image/webp;base64,...') center center / cover no-repeat;">
```

## Cache service worker

A [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) to cache assets files locally can be automatically generated. It will include all bundled assets. This is particularly useful for [PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).

```python
assets = AssetsPipeline(app, ..., cache_worker=True)
```

The worker will be automatically registered on page load when using `{% asset_tags %}`.

## Customizing esbuild

To customize esbuild, you can use a custom script. When the script is ran, it will be provided environment variables that can help with configuration.

```python
assets = AssetsPipeline(app, ..., esbuild_script="esbuild.mjs")
```

Use the command `flask assets generate-esbuild-script` to generate a base script that shows you the env vars available.

## Compatibility with CSP

Common Content Security Policies prevents inline script to be executed. However, inline scripts are used for the live reloader and the cache worker.

There is a mechanism to allow inline scripts via the used of a "nonce".

You can provide a nonce like so: `{% asset_tags(nonce="...") %}

## Configuration

### Core

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_BUNDLES | bundles | List of assets to build | |
| ASSETS_INCLUDE | include | List of assets to include in your page | All assets in bundles |
| ASSETS_IMPORT_MAP | import_map | ES Modules import map | {} |
| ASSETS_FOLDER | assets_folder | Search path for asset files | The app static_folder |
| ASSETS_BUNDLE_OUTPUT_FOLDER | bundle_output_folder | The output folder for bundled files relative to the static folder | dist |
| ASSETS_STATIC_OUTPUT_FOLDER | static_output_folder | The output folder for static files copied from the assets folder | |
| ASSETS_STAMP | stamp_assets | Whether to stamp filenames with the file's hash when copying files from the assets folder to the static folder | True |
| ASSETS_ROUTE_TEMPLATE | route_template | The template to use for frontend routes | frontend_route.html |
| ASSETS_MAPPING_FILE | mapping_file | Location of the mapping file to resolve assets filename to their built equivalent | assets.json |
| ASSETS_LIVERELOAD_PORT | livereload_port | Port onto which to start the livereloading server | 8000 |

### Esbuild

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_ESBUILD_SCRIPT | esbuild_script | Use a custom script calling esbuild insteaf of esbuild's cli | |
| ASSETS_ESBUILD_ARGS | esbuild_args | Additional esbuild arguments | [] |
| ASSETS_ESBUILD_BIN | esbuild_bin | esbuild binary location | npx esbuild |
| ASSETS_ESBUILD_SPLITTING | esbuild_splitting | Use the --splitting option | True |
| ASSETS_ESBUILD_TARGET | esbuild_target | Set the target option for esbuild (use a list) | [] |
| ASSETS_ESBUILD_ALIASES | esbuild_aliases | Esbuild aliases | {} |
| ASSETS_ESBUILD_EXTERNAL | esbuild_external | Esbuild external imports | [] |

### Tailwind

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_TAILWIND | tailwind | Tailwind input css file | |
| ASSETS_TAILWIND_ARGS | tailwind_args | Additional tailwind arguments | [] |
| ASSETS_TAILWIND_BIN | tailwind_bin | tailwindcss binary location | npx tailwindcss |
| ASSETS_TAILWIND_EXPAND_ENV_VARS | tailwind_expand_env_vars | Whether to expand env vars in tailwind input file | False |

### Inline assets

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_INLINE | inline | Whether to extract assets from templates | False |
| ASSETS_INCLUDE_INLINE_ON_DEMAND | include_inline_on_demand | Whether to only include inline assets when the template is used | False |
| ASSETS_INLINE_TEMPLATE_EXTS | inline_template_exts | File extensions to process when looking for templates | .html .jinja |

### CDN

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_CDN_HOST | cdn_host | CDN hostname |  |
| ASSETS_CDN_ENABLED | cdn_enabled | Whether to use the cdn | True in prod if cdn_host is set |

### Images

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_IMAGES_FOLDER | images_folder | Folder relative to static dir when a separate assets folder is not used |
| ASSETS_IMAGES_OUTPUT_FOLDER | images_output_folder | Folder relative to static dir where processed images are saved | dist/img
| ASSETS_IMAGES_SIZES | images_sizes | True for default set of sizes or a list of width in pixels | False
| ASSETS_IMAGES_DEFAULT_SIZE | images_default_size | Default size out of resized images to use | 
| ASSETS_IMAGES_OPTIMIZE | images_optimize | Whether to optimize all images | False
| ASSETS_IMAGES_WEBP | images_webp | Whether to convert images to Webp | False
| ASSETS_IMAGES_PLACEHOLDER | images_placeholder | Compute a placeholder data URI for images | True

### Node packages

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_EXPOSE_NODE_PACKAGES | expose_node_packages | Node packages to expose to the frontend via the import map | [] |
| ASSETS_NODE_MODULES_PATH | node_modules_path | Path to the node_modules directory | node_modules |
| ASSETS_COPY_FILES_FROM_NODE_MODULES | copy_files_from_node_modules | Mapping of src/dest files to copy from node modules | {} |

### Cache worker

| Config key | Extension argument | Description | Default |
| --- | --- | --- | --- |
| ASSETS_CACHE_WORKER | cache_worker | Whether to generate and register a cache service worker (useful for PWAs) | False |
| ASSETS_CACHE_WORKER_NAME | cache_worker_name | Name of the cache | Random |
| ASSETS_CACHE_WORKER_URLS | cache_worker_urls | Additional urls to cache | [] |
| ASSETS_CACHE_WORKER_FILENAME | cache_worker_filename | Filename of the service worker | cache-worker.js |
| ASSETS_CACHE_WORKER_REGISTER | cache_worker_register | Whether to register the service worker | True if cache_worker is true |

## Commands

| Command | Description |
| --- | --- |
| assets dev | Start the build process in development |
| assets build | Build your assets for production |
| assets extract | Extract assets from templates |
| assets init-tailwind | Create the tailwind config |
| assets generate-esbuild-script | Generate a script to run esbuild allowing you to customize the build |
| assets esbuild-script | Run the esbuild script |

## Using another bundler

Flask-Assets-Pipeline is built as a generic assets pipeline system with esbuild deeply integrated. Other bundlers could be used instead of esbuild.

Create a new builder similar to [`EsbuildBuilder`](https://github.com/hyperflask/flask-assets-pipeline/blob/main/src/flask_assets_pipeline/builders/esbuild.py).

Use your builder instead of esbuild: `AssetsPipeline(app, ..., bundle_builder=YourCustomBuilder())`