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.
myapp/
: Your Flask Python project.vite/
manifest.json
: The location of the generated asset manifest in production
static/
dist/
: The location of the generated assets in production
src/
: The source directory of our TypeScript and SCSSmymodule/
index.ts
: The entrypoint for a modulestyle.scss
: A SCSS stylesheet that can be included in theindex.ts
file
another_module/
index.ts
: Another entrypoint
vite-env.d.ts
: Our Vite module type definitions (for importing assets)
package.json
: Our Node package listtsconfig.json
: The TypeScript configurationvite.config.js
: The Vite configuration
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:
dev
: To start the Vite dev server (onlocalhost:5173
by default)build
: To type check our TypeScript and build for production
{
...
"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.
- The CORS configuration is set to localhost on the default Flask port (5000), this allows us to request assets from the dev server during development.
- The build manifest is enabled, this is used in production to find the files to include (example below).
- Using the
glob
package, we can import all of theindex.ts
files in thesrc
directory. These are the individual module entrypoints. - Preserving entry signatures allows us to keep named exports, if we need to call something in the bundled from the frontend.
- The last few options will place the built assets in our Flask static directory. Setting
emptyOutDir: false
means that it won't clear all our source files.
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:
get_module_path
: This returns the path to a given module by name, without thestatic
prefix so that I can be requested by the browser. If the dev server is running, we can request the file from that instead.include_module_style
: This returns the markup of stylesheet links associated with a module (if available)
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.