feat(ext-vite): gzip at build time, fall back to the index only for navigations

The Maven plugin now writes a maximally compressed .gz beside every text file of 1 KB
and more, so the server compresses nothing and boot only reads the files: about 50 ms
for Glossa's 500. Hashed files under assets/ skip the ETag, which nothing ever asks for.

A path that is no file gets index.html only when the request's Accept names text/html,
as a browser navigation does. Everything else, an API call to a missing route included,
gets the app's own 404 instead of the index, and a dot in a client route no longer
matters. The base path itself always serves the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-09-22 15:54:19 +00:00
co-authored by Claude Opus 5
parent 580417e952
commit 680bbca8c7
7 changed files with 100 additions and 70 deletions
+16 -11
View File
@@ -91,29 +91,31 @@ server: {
Anywhere `Flash.DEV` is false, the build is read once at boot from `flash-vite/` on the classpath,
whether that is a directory (`target/classes`) or a jar. A missing build fails the boot and names
the plugin. Everything a response carries is prepared at boot, headers included, so serving
allocates nothing.
the plugin. Compression already happened in the build, so boot only reads files and hashes the few
that revalidate: about 50 ms for a 500-file app. Everything a response carries is prepared at
boot, headers included, so serving allocates nothing.
`GET` and `HEAD` on `basePath/**` answer as follows. Backend routes still win, because Flash
prefers specific routes over the wildcard.
| Request | Answer |
|---|---|
| a built file under `assets/` | `Cache-Control: public, max-age=31536000, immutable` |
| `basePath` itself (`/`) | `index.html`, whatever the client accepts |
| a built file under `assets/` | `Cache-Control: public, max-age=31536000, immutable`, no `ETag` (it is never asked for again) |
| any other built file (`index.html`, `favicon.svg`, …) | `Cache-Control: no-cache`, revalidated by `ETag` |
| a path with no file extension (`/content/2`) | `index.html`, so the client-side router takes it |
| a path with an extension that matches no file | `404` |
| no such file, and `Accept` names `text/html` (a browser navigating to `/content/2`) | `index.html`, so the client-side router takes it |
| no such file otherwise (a `fetch` to `/api/typo`, a missing script) | the app's own `404` |
| `If-None-Match` with the current `ETag` | `304` with `ETag` and `Cache-Control`, no body |
| `HEAD` | the `GET` headers, `Content-Length` included, no body |
Text formats of 1 KB and more (html, js, css, json, svg, fonts, wasm, …) are gzipped once at boot
and sent to clients whose `Accept-Encoding` allows gzip (`gzip;q=0` does not), with
`Vary: Accept-Encoding`. Images and woff are sent as they are, because they are compressed already.
A `.gz` beside a file is its gzipped form: the plugin writes one for every text file of 1 KB and
more, and it is sent to clients whose `Accept-Encoding` allows gzip (`gzip;q=0` does not), with
`Vary: Accept-Encoding`.
Known limits:
- A client-side route whose last segment has a dot (`/users/ada.lovelace`) is treated as a missing
file. Keep dots out of client routes, or route them under `basePath` differently.
- A browser navigating straight to an API path that does not exist gets the app, which shows its
own not-found screen. Only an HTML navigation falls back, so API clients always get the 404.
- Every file is held in memory, gzip included. That suits an app's own frontend. Large media belongs
on a CDN or behind its own route.
@@ -124,7 +126,10 @@ The `build` goal is bound to `prepare-package`, so `mvn test` never needs Node,
1. installs the dependencies with the lockfile pinned;
2. runs `<pm> run build` in the project;
3. replaces `target/classes/flash-vite/` with the project's `dist/`.
3. replaces `target/classes/flash-vite/` with the project's `dist/`;
4. writes a gzip `.gz` at maximum compression beside every html, js, css, json, map, svg, txt,
xml, webmanifest, font and wasm file of 1 KB and more, and keeps it only when it is smaller.
Images and woff are left alone, because they are compressed already.
Missing Node or package manager, a failing install or build, or a build that leaves no
`dist/index.html` fails the Maven build with the reason. The package manager's own output shows in