Bundling Typescript and SCSS using Vite with Python and Flask

Bundling Typescript and SCSS using Vite with Python and Flask

Vite is a great tool for bundling JS modules and stylesheets. Here's how to use it with the Flask microframework.

March 16, 2025
Table of Contents

Vite is a frontend build tool used for bundling and minifying assets such as JavaScript and CSS. It's typically used in conjunction with a JavaScript framework, however here I will demonstrate how to use it within a Flask project.

Flask is a great microframework for server rendered applications, however sometimes you may need to include certain JavaScript libraries on the frontend. Using a bundler, we can import any compatible JavaScript module and shrink it into a compact and optimised JavaScript bundle.

Specifically, this example shows how to compile TypeScript and SCSS into small asset modules for the web. To accompany this article, you will find a GitHub repository and a live demonstration showing the use of a rich chart library on the frontend of a Flask app.

Project structure

In this example I will use the following structure: a Flask app using the factory pattern and a src directory for all the frontend assets.

The JavaScript modules are split into multiple entrypoints (mymodule and another_module), this allows us to write specific JavaScript and CSS for our pages without including a large bundle on each. Vite will handle the splitting into reusable chunks.

Creating the configuration files

Project configuration

Start by installing the required Node packages:

npm init # create a package.json file if it doesn't exist
npm i glob vite sass typescript

Then add the following scripts to the package.json file:

{
    ...
    "scripts": {
        "dev": "vite",
        "build": "tsc && vite build"
    }
    ...
}

TypeScript configuration

In our TypeScript configuration make sure to include the following compiler options and include the source directory:

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "lib": [
            "ES2020",
            "DOM",
            "DOM.Iterable"
        ],
        ...
        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "isolatedModules": true,
        "moduleDetection": "force",
        "noEmit": true
    },
    ...
    "include": [
        "src"
    ]
}

Here's an explanation of the options used in this example:

Option Description
target The target is ES2020, this provides a good compromise between output size and compatibility
module Since the code is bundled, we can use the very latest features with ESNext
lib Include the type definitions for the following types that we assume are available at runtime (such as the DOM and ES2020 since this is the target)
moduleResolution Use bundler so that imports work as we would expect
allowImportingTsExtensions Set this to true to that TypeScript files can import each other
isolatedModules Set to true to prevent scripts from being imported into the global namespace
moduleDetection Set to force to treat every file as a module
noEmit Set to true to prevent intermediate files from being generated. Not needed with a bundler.

Vite configuration

Here's an example Vite configuration.

import {defineConfig} from "vite";
import {globSync} from "glob";
import path from "node:path";

export default defineConfig({
    server: {
        cors: {
            origin: ["http://127.0.0.1:5000", "http://localhost:5000"],
        },
    },
    build: {
        manifest: true,
        rollupOptions: {
            input: Object.fromEntries(globSync("./src/**/index.ts").map(file => [
                path.dirname(path.relative("src", file)),
                file 
            ])),
            preserveEntrySignatures: "strict",
        },
        outDir: "myapp",
        emptyOutDir: false,
        assetsDir: "static/dist"
    },
});

Here's an example of the generated asset manifest in production. This will be read by the Flask app to determine which assets to include. Keep in mind, some modules may include CSS and others might not. Also, particularly large stylesheets, might be split into multiple files for performance reasons.

{
    "src/another_module/index.ts": {
        "file": "static/dist/another_module-BymvPiom.js",
        "name": "another_module",
        "src": "src/another_module/index.ts",
        "isEntry": true
    },
    "src/mymodule/index.ts": {
        "file": "static/dist/mymodule-CfmbzKti.js",
        "name": "mymodule",
        "src": "src/mymodule/index.ts",
        "isEntry": true,
        "css": [
            "static/dist/mymodule-19t1bAyr.css"
        ]
    }
}

Modules

In order to support CSS/SCSS imports in our modules, we must inherit the Vite types within the src/vite-env.d.ts file:

/// <reference types="vite/client" />

Now in a module file such as src/mymodule/index.ts, we can include SCSS and any JavaScript that we want:

import "./style.scss";

document.getElementById("message")!.innerText = "Message from JavaScript";

Integrating Vite with Flask

Here's an example of how to include the static assets in our Flask app using the dev server in development and the asset manifest in production.

Adding the Vite dev server configuration

Include the following keys in the Development and Production Flask configs. With VITE_DEV_SERVER equal to None we assume that the dev server is not used.

class DevelopmentConfig:
   VITE_DEV_SERVER = "http://localhost:5173"
class ProductionConfig:
   VITE_DEV_SERVER = None

Using context processors to read the manifest

Using context processors, we can add functions that will be available in our Jinja templates to include the JavaScript and CSS files. They assume that our static assets live in the static directory.

This example shows two processors:

The manifest access functions are cached, this isn't strictly necessary, but it doesn't cost anything.

import functools
import json
import os

from typing import Optional

from flask import current_app

from markupsafe import Markup


@functools.lru_cache(maxsize=1)
def _get_asset_manifest() -> dict[str, dict]:
    """
    Get the asset manifest (in myapp/.vite/manifest.json)
    """
    with open(
        os.path.join(current_app.root_path, ".vite", "manifest.json"),
        "r",
        encoding="utf-8",
    ) as file:
        return json.load(file)


@functools.lru_cache(maxsize=None)
def _get_manifest_chunk(name: str) -> Optional[dict]:
    """
    Get a manifest entry by name
    """
    manifest = _get_asset_manifest()
    return next((c for c in manifest.values() if c["name"] == name), None)


def module_path_processor(name: str) -> str:
    """
    Context processor to get the path of a module as defined in the asset manifest
    If running in development mode, return the file served from the Vite dev server
    """
    vite_dev_server = current_app.config["VITE_DEV_SERVER"]

    if vite_dev_server is not None:
        return f"{vite_dev_server}/src/{name}/index.ts"

    chunk = _get_manifest_chunk(name)

    if chunk is None:
        return ""

    return chunk["file"][len("static") :]


def module_style_processor(name: str) -> Markup:
    """
    Context processor to get stylesheets associated with a module (applies only to production)
    If running in development mode, stylesheets are served dynamically by the Vite dev server
    """
    if current_app.config["VITE_DEV_SERVER"] is not None:
        return Markup()

    chunk = _get_manifest_chunk(name)

    if chunk is None or "css" not in chunk:
        return Markup()

    result = ""

    for css in chunk["css"]:
        dist_path = css[len("static") :]
        result += f'<link rel="stylesheet" href="{dist_path}">'

    return Markup(result)

Make sure to register the context processors in the app or blueprint:

app.context_processor(
    lambda: {
        "get_module_path": module_path_processor,
        "include_module_style": module_style_processor,
    }
)

Including assets in the templates

In our base template, the main dev server module should be included if available. This allows for hot reloads of our CSS and JavaScript.

<!doctype html>
<html>
    <head>
        {% block head %}{% endblock %} 
    </head>
    <body>
        {% block content %}{% endblock %}
        
        {% if config["VITE_DEV_SERVER"] is not none %}
            <!-- Include the Vite dev server module -->
            <script type="module" src="{{ config['VITE_DEV_SERVER'] }}/@vite/client"></script>
        {% endif %}
    
        {% block foot %}{% endblock %} 
    </body>
</html>

In any template where we want to include a module, we can simply import the stylesheet in the head and the JavaScript at the bottom of the page. Using the previously defined context processors, they can be referred to by name.

<!-- Include the associated stylesheets in the document head -->
{% block head %}
   {{ include_module_style("mymodule") }}
{% endblock %}

<!-- Include the bundled JavaScript at the end of the document -->
{% block foot %}
   <script type="module" src="{{ get_module_path('mymodule') }}"></script>
{% endblock %}

<!-- Alternatively, call a function within the module -->
{% block foot %}
    <script type="module">
        import { myFunction } from "{{ get_module_path('mymodule') }}";
        myFunction("do something");
    </script>
{% endblock %}

Building for production

When building for production using the npm build command, the assets will be bundled and placed in the static directory. The asset manifest will also be generated and placed in the .vite directory.

vite v6.2.2 building for production...
✓ 565 modules transformed.
flaskvite/.vite/manifest.json                              0.95 kB │ gzip:   0.29 kB
flaskvite/static/dist/bar_chart-DqQksVyv.css               0.01 kB │ gzip:   0.03 kB
flaskvite/static/dist/line_chart-DAnG9F2j.css              0.02 kB │ gzip:   0.04 kB
flaskvite/static/dist/main-19t1bAyr.css                    2.27 kB │ gzip:   0.88 kB
flaskvite/static/dist/main-CfmbzKti.js                     0.07 kB │ gzip:   0.09 kB
flaskvite/static/dist/bar_chart-DMhJvRAs.js               17.48 kB │ gzip:   6.61 kB
flaskvite/static/dist/line_chart-BymvPiom.js              27.04 kB │ gzip:  10.51 kB
flaskvite/static/dist/installCanvasRenderer-DxXfmLY6.js  480.16 kB │ gzip: 163.17 kB
✓ built in 1.61s

Conclusion

In conclusion, Vite is a great tool for handling our JavaScript and CSS. With very little configuration, it provides a seamless developer experience for handling live reloads and production bundling. It provides a faster and easier to configure workflow when compared to alternative tools such as Webpack.

To expand on this example, if your bundles are particularly large, you could benefit from implementing a context processor to add modulepreload tags to your templates using the imports section of the manifest. This will instruct the browser to prefetch certain imported modules before the entrypoint is fetched and parsed.


Any comments or suggestions about this article? Feel free to contact me!

Latest posts

Basic Ubuntu Server hardening and best security practices

Here are some ways to make a standard Ubuntu Server install more secure. These are some common server hardening tips.

March 16, 2025
Managing secrets in Docker Compose and GitHub Actions deployments

When deploying Docker Compose applications, here's how you can manage secrets without embedding them in your containers

October 20, 2024
Deploy Docker Compose applications with zero downtime using GitHub Actions

This example demonstrates Blue-Green deployments using Docker Compose and GitHub Actions, deploy an app with zero downtime

July 21, 2024