feat(ext-web-bundler): add build-time asset scanning and static-frontend strategy

Introduces AssetDirectoryScanner, StaticFrontendStrategy, and WebBundlerBuild for
build-time asset discovery, plus config/docs updates for frontend-type resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-11 00:22:26 +00:00
co-authored by Claude Sonnet 5
parent fa0a2d79b4
commit 8ece9975de
16 changed files with 399 additions and 54 deletions
@@ -13,7 +13,14 @@ Builder shortcuts:
- `.assetsFromClasspath("web/dist")`
Classpath source requires `asset-manifest.json` generated at build time.
The developer does not maintain this file manually.
The developer does not maintain this file manually — generate it with `WebBundlerBuild`
(`dev.relism.flash.ext.webbundler.WebBundlerBuild`), which scans a prebuilt directory (a Vite
`dist/` or a `STATIC` asset folder) and writes the manifest into it, ready to be picked up as a
classpath resource once that directory lands under `target/classes`. See `build-time.md`.
`WebBundlerBuild` reuses the exact same etag/mimeType/immutable computation `FilesystemAssetsSource`
uses at runtime, so a file served from disk in dev and the same file served from the classpath in
prod get identical cache semantics.
Production startup is fail-fast if:
@@ -0,0 +1,43 @@
# Build-Time Manifest Generation
`WebBundlerBuild` turns an already-built directory into the `asset-manifest.json` that
`ClasspathAssetsSource` needs (see `asset-sources.md`). It does not run a frontend build itself —
it only scans a directory that already contains the final files:
- `VITE`: point it at whatever `dist/` the existing frontend build tooling already produces.
- `STATIC`: point it directly at the static asset folder — there's no separate build step.
It's meant to run once per build, from the consumer project's own build, not from the running
application (`ClasspathAssetsSource` is explicitly unsupported in DEV — see `dev-lifecycle.md`).
## Wiring it into a Maven build
No dedicated Flash5 Maven plugin — `WebBundlerBuild` is a plain class with a `main`, invoked via
the standard `exec-maven-plugin`, bound to run before the resources are packaged:
```xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>web-bundler-manifest</id>
<phase>process-classes</phase>
<goals><goal>java</goal></goals>
<configuration>
<mainClass>dev.relism.flash.ext.webbundler.WebBundlerBuild</mainClass>
<arguments>
<argument>${project.build.outputDirectory}/web/dist</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
```
This assumes the built frontend (`web/dist/`, or a static folder) is already copied under
`target/classes/web/dist` by that point — e.g. via `maven-resources-plugin`'s `copy-resources`
goal, or by running the frontend build with an output directory that points there directly. Once
the manifest is written alongside those files, they're just classpath resources: a plain `mvn
package` (or `maven-shade-plugin` for a fat jar) picks them up with no further configuration, and
the app can then be configured with `.assetsFromClasspath("web/dist")`.
@@ -6,7 +6,7 @@ Key fields:
- `runtimeMode`: `PROD`, `ENV`, `AUTODETECT`
- `operationMode`: `ORCHESTRATE_ONLY`, `MANAGED`
- `frontendType`: currently `VITE`
- `frontendType`: `VITE`, `STATIC` — see `frontend-selection.md`
- `packageManager`: `NPM`, `PNPM`, `YARN`, `BUN`
- `installPolicy`: `AUTO_IF_LOCK_HASH_CHANGED`, `NEVER`
- `loggingMode`: `MERGED`, `SEPARATE`, `QUIET`, `VERBOSE`
@@ -25,6 +25,12 @@ Or by direct source object:
Validation is fail-fast:
- invalid `devPort`
- blank/invalid watch entries
- invalid `devPort` (only when `frontendType` requires orchestration — skipped for `STATIC`)
- blank/invalid watch entries (same — skipped for `STATIC`)
- invalid `basePath`
`frontendType(...)` has side effects on other defaults, same pattern as `packageManager(...)`
resetting `watchList`: it also resets `operationMode` (`MANAGED` for `STATIC`, `ORCHESTRATE_ONLY`
otherwise) and `assetsSource` (`webRoot` itself for `STATIC`, `webRoot/dist` otherwise). Call
`.frontendType(...)` before any explicit `.operationMode(...)`/`.assetsSource(...)`/`.assetsFrom*(...)`
override, or the later call wins.
@@ -4,7 +4,29 @@ Frontend integration is explicit through `frontendType`.
- No heuristic detection in v1.
- Deterministic mapping: `FrontendType -> FrontendStrategy`.
- Current built-in strategy: `VITE`.
- Built-in strategies: `VITE`, `STATIC`.
## VITE
Orchestrates a dev server process in DEV, serves a prebuilt directory in PROD. See `dev-lifecycle.md`.
## STATIC
For files served as-is — no dev server, no package manager, no build step, no watch loop.
`STATIC` never orchestrates, in DEV or PROD: it always loads `assetsSource` directly and serves it,
the same code path `VITE` only uses in PROD. Editing a file during a running dev session requires a
restart to be picked up (assets are preloaded once, same as `VITE`'s prod serving — no hot reload).
Setting `.frontendType(FrontendType.STATIC)` also switches two other defaults (see `configuration.md`):
`operationMode` becomes `MANAGED` and `assetsSource` defaults to the `webRoot` itself instead of a
`dist` subdirectory — a minimal STATIC config is just:
```java
WebBundlerConfig.builder()
.frontendType(FrontendType.STATIC)
.webRoot(Path.of("public"))
.build()
```
Extension points:
@@ -10,3 +10,12 @@
- `ORCHESTRATE_ONLY`: only orchestrates dev tooling.
- `MANAGED`: enables production serving + SPA fallback routes.
`FrontendType.STATIC` defaults `operationMode` to `MANAGED` (see `frontend-selection.md`) — `STATIC`
has no dev tooling to orchestrate, so `ORCHESTRATE_ONLY` would make the extension a no-op for it.
## Orchestration
Whether DEV mode spawns a dev-server process at all is a separate axis from Runtime Mode: it also
depends on `frontendType`. `VITE` orchestrates in DEV; `STATIC` never does, in DEV or PROD — it
always loads and serves `assetsSource` directly, the same path `VITE` only takes in PROD.