Skip to main content

FhVite

Vite plugin for FastHTML - automatic bundling, minifcation and script injection of static assets

Imports

File configuration

File init


source

setup_files

def setup_files(
    root_dir, entry_file, use_monster:bool=True
):

Create missing Vite files and install project dependencies

setup_files(root_dir='vite', entry_file='index.js')
Created: vite/vite.config.js
Created: vite/package.json
Created: vite/src/style.css
Created: vite/src/index.js

added 113 packages, and audited 114 packages in 7s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
for path in _monster_temp.keys():
    print('#'+path, (Path('vite')/path).read_text()[:100], sep='\n')
    print('---')
#vite.config.js
import franken from "franken-ui/plugin-vite";
import tailwindcss from "@tailwindcss/vite";
import { 
---
#package.json
{
  "name": "fastvite",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {

---
#src/style.css
@import "tailwindcss";
@import "franken-ui/css/franken-ui.css";
@source "../../";

@theme {
  --colo
---
#src/index.js
import "vite/modulepreload-polyfill";
import "./style.css";
---

Create script injections

subprocess.run('pybun run build', cwd='vite', shell=True, check=True)

> fastvite@0.0.0 build
> vite build

vite v7.0.4 building for production...
transforming...
✓ 3 modules transformed.
rendering chunks...
computing gzip size...
../dist/.vite/manifest.json          0.19 kB │ gzip:  0.14 kB
../dist/assets/index-DwAzhI66.css  215.53 kB │ gzip: 25.35 kB
../dist/assets/index-DSGW5Iaw.js     0.71 kB │ gzip:  0.40 kB
✓ built in 101ms
CompletedProcess(args='npm run build', returncode=0)
print(*(o for o in _mk_scripts(dirname="vite", entry_file='index.js')))
<link rel="stylesheet" href="/vite/assets/index-DwAzhI66.css"> <script type="module" src="/vite/assets/index-DSGW5Iaw.js"></script>

App configuration

The dist mount is served with fasthtml’s StaticImmutable. Vite’s output uses content-hashed filenames, so a changed file always gets a new URL, browsers can safely cache forever, and revalidating would be pure waste. (.vite/manifest.json is served under the same mount without a hashed name, but it’s only read from disk by _mk_scripts, never fetched by browsers.)

With run_build=True, startup creates missing setup files, installs dependencies with pybun install and runs pybun run build. Installation follows the project’s lockfile. directory names the project root; setup, builds and manifest reads use its dirname subdirectory regardless of the process working directory.

With run_build=False, startup reads the existing manifest without creating files, installing dependencies or building. Use this mode when deployment has already built the assets.

With dev=True, startup installs dependencies and runs Vite on localhost with hot module replacement. port defaults to 5173. An occupied port fails startup rather than selecting another port. The app header loads Vite’s client and the source entry point instead of a build manifest. Shutdown stops Vite and its children, including when the app’s own startup fails. run_build applies only when dev=False.


source

add_vite

def add_vite(
    app:fasthtml.core.FastHTML, entry_file:str='index.js', dirname:str='frontend', use_monster:bool=True,
    directory:str='.', run_build:bool=True, dev:bool=False, port:int=5173
):

Configure app to use vite; dev=True owns a localhost Vite server with hot module replacement

from fasthtml.common import *
app = FastHTML()
app
<fasthtml.core.FastHTML>
app = FastHTML(surreal=False)
rt = app.route
#app.hdrs
add_vite(app, dirname='vite', dev=True)

> fastvite@0.0.0 build
> vite build

vite v7.0.4 building for production...
transforming...
✓ 3 modules transformed.
rendering chunks...
computing gzip size...
../dist/.vite/manifest.json          0.19 kB │ gzip:  0.14 kB
../dist/assets/index-DwAzhI66.css  215.53 kB │ gzip: 25.35 kB
../dist/assets/index-DSGW5Iaw.js     0.71 kB │ gzip:  0.40 kB
✓ built in 91ms
app.hdrs
[meta((),{'charset': 'utf-8'}),
 meta((),{'name': 'viewport', 'content': 'width=device-width, initial-scale=1, viewport-fit=cover'}),
 link((),{'rel': 'stylesheet', 'href': '/static/assets/index-DwAzhI66.css'}),
 script(('',),{'type': 'module', 'src': '/static/assets/index-DSGW5Iaw.js'}),
 link((),{'rel': 'preconnect', 'href': 'https://cdn.jsdelivr.net'}),
 script(('\n    const htmlElement = document.documentElement;\n\n    const __FRANKEN__ = JSON.parse(\n    localStorage.getItem("__FRANKEN__") || "{}",\n    );\n\n    if (\n    __FRANKEN__.mode === "dark" ||\n    (!__FRANKEN__.mode &&\n        window.matchMedia("(prefers-color-scheme: dark)").matches)\n    ) {\n    htmlElement.classList.add("dark");\n    } else {\n    htmlElement.classList.remove("dark");\n    }\n\n    htmlElement.classList.add(__FRANKEN__.theme || "uk-theme-zinc");\n    htmlElement.classList.add(__FRANKEN__.radii || "uk-radii-md");\n    htmlElement.classList.add(__FRANKEN__.shadows || "uk-shadows-md");\n    htmlElement.classList.add(__FRANKEN__.font || "uk-font-sm");',),{}),
 script(('',),{'src': 'https://cdn.jsdelivr.net/npm/franken-ui@2.1.0-next.14/dist/js/core.iife.js', 'type': 'module'}),
 script(('',),{'src': 'https://cdn.jsdelivr.net/npm/franken-ui@2.1.0-next.14/dist/js/icon.iife.js', 'type': 'module'}),
 script((),{'src': 'https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js'}),
 script((),{'src': 'https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js'}),
 script(("\n    function sendmsg() {\n        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n    }\n    window.onload = function() {\n        sendmsg();\n        document.body.addEventListener('htmx:afterSettle',    sendmsg);\n        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n    };",),{})]

Testing in a running app

server = JupyUviAsync(app)
await server.start()
with urlopen('http://127.0.0.1:5173/vite/@vite/client') as response: test_eq(response.status, 200)
Show = partial(HTMX, app=app)

A second app using the same Vite port fails startup. It does not attach to the running Vite server.

other = FastHTML()
add_vite(other, dirname='vite', dev=True)
with expect_fail(RuntimeError, contains='Vite exited with status'):
    async with other.router.lifespan_context(other): pass
with urlopen('http://127.0.0.1:5173/vite/@vite/client') as response: test_eq(response.status, 200)
@rt
def index():
    return Titled('FhVite Test App', 
                  H1('This is a test!'), 
                  Div(Button('Click Me!', cls="uk-btn-primary uk-btn-xs"))
                 )
Show('/')
await server.stop()
!rm -rf vite