From e65b5600a979110e4a07ee9cbc5cf2a83d553c36 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Wed, 1 Jul 2026 09:47:50 +0200 Subject: [PATCH 1/9] docs: add WebJars support design (Struts core + bootstrap plugin consumer) Requirements/design spec for adding first-class WebJars support to Struts core, served through the static-content pipeline with version-less path resolution, and consuming it from the struts2-bootstrap plugin to replace the ~2000 manually-vendored Bootstrap/bootstrap-icons files. Co-Authored-By: Claude Opus 4.8 --- ...026-07-01-struts-webjars-support-design.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-01-struts-webjars-support-design.md diff --git a/docs/superpowers/specs/2026-07-01-struts-webjars-support-design.md b/docs/superpowers/specs/2026-07-01-struts-webjars-support-design.md new file mode 100644 index 0000000..ec1c815 --- /dev/null +++ b/docs/superpowers/specs/2026-07-01-struts-webjars-support-design.md @@ -0,0 +1,164 @@ +# Design: WebJars Support in Struts (framework) + Bootstrap plugin consumer + +**Date:** 2026-07-01 +**Status:** Approved design, ready for implementation planning +**Author:** brainstormed with Claude Code + +## Summary + +Add first-class **WebJars support to Struts core** so that client-side libraries +packaged as WebJars (`org.webjars:*`, shipped under +`META-INF/resources/webjars///…`) can be referenced from templates +and tags by a **version-less logical path** and served through Struts' existing +static-content pipeline. + +The **struts2-bootstrap plugin is the first consumer**: it drops its ~2000 +manually-vendored Bootstrap and bootstrap-icons files in favour of +`org.webjars` dependencies, letting Renovate (already configured in this repo via +`.github/renovate.json`) auto-bump Bootstrap going forward. + +This document is the requirements input for two separate implementation sessions: + +1. **Struts framework session** (apache/struts repo) — build the WebJars mechanism. +2. **Plugin session** (this repo) — consume the mechanism. + +## Motivation + +Today the plugin vendors the full Bootstrap dist and the complete bootstrap-icons +set (~2000 SVGs + fonts + CSS/JS) directly under +`struts2-bootstrap-plugin/src/main/resources/template/bootstrap/`. Every Bootstrap +release requires a manual re-download and a noisy 2000-file commit (see prior +upgrade `cb6c9b3`, "Upgrade to bootstrap version 5.3.5"). Assets are served by +Struts' `DefaultStaticContentLoader` from the classpath under the UI +`staticContentPath`. + +WebJars turn "manually re-vendor every release" into "Renovate opens a one-line +`pom.xml` bump PR". Because the repo already relies on Renovate for dependency +updates, this aligns the Bootstrap upgrade cadence with every other dependency. + +## Decisions + +| # | Decision | Choice | Rationale | +|---|----------|--------|-----------| +| Q0 | **Serving model** | **Struts serves** webjar assets through its static-content pipeline (`DefaultStaticContentLoader`), reusing existing content-type/caching/ETag handling. | Framework owns caching/security; works even where the servlet container does not auto-serve `META-INF/resources/`. Consistent with how Struts already serves its bundled assets. | +| 1 | **Version resolution** | Depend on **`webjars-locator-lite`** to map version-less path → versioned classpath resource. | Purpose-built for frameworks, dependency-light (adopted by Spring); avoids Struts reinventing classpath scanning. | +| 2 | **Framework→plugin contract** | Public **`WebJarUrlProvider` interface** (resolvable via the Struts container) **+ a thin `` tag and `<@s.webjar>` FreeMarker macro** built on top. | Gives plugins a clean injectable Java seam and template ergonomics. The plugin's `head.ftl` calls the macro. | +| 3 | **URL prefix** | Serve under the **existing static content path**, e.g. `${staticContentPath}/webjars/bootstrap/css/bootstrap.min.css`. | Reuses the already-wired static dispatcher; no new servlet mapping; single enable/disable switch. | +| 4 | **Security / allowlist** | Hard-constrain resolution to the **`META-INF/resources/webjars/` root** with path normalization (block `..` traversal). **Optional allowlist** of webjar names via a constant; default = all webjars on classpath. | Struts now streams classpath bytes, so traversal protection is mandatory; allowlist is opt-in defense-in-depth. | +| 5 | **Cache-busting** | **Drop** the plugin's current `?s2b=${version}` query param. | The resolved version already lives in the URL path, which is inherently cache-stable. | +| 6 | **Min Struts version & degradation** | Land in the **next Struts 7.x minor**. New plugin major **hard-requires** it — **no** vendored fallback path. | A fallback doubles maintenance; a clean version gate is simpler. | +| 7 | **Plugin-side scope** | **Drop vendored Bootstrap + bootstrap-icons**, add `org.webjars` dependencies. **Keep `validation.js` / `validation.min.js`** vendored. | `validation.js` is the plugin's own custom form-validation script, not a WebJar; everything else moves. | + +## Part 1 — Struts framework requirements + +### 1.1 Resource resolution + +- Given a logical path `/` (e.g. + `bootstrap/css/bootstrap.min.css`), resolve to the concrete classpath resource + `META-INF/resources/webjars///` using + `webjars-locator-lite`. +- Resolution runs once and is cached (the locator scans at startup). +- If the webjar or path cannot be resolved, fail closed (404 / no URL emitted), + never fall through to arbitrary classpath serving. + +### 1.2 Serving + +- Extend `DefaultStaticContentLoader` (or add a sibling loader wired into the same + static dispatch) to handle requests under + `/webjars/**`. +- Reuse existing content-type detection, `Last-Modified`/ETag, and browser-cache + behaviour governed by `struts.serve.static` and + `struts.serve.static.browserCache`. +- **Verify against the current apache/struts source** how `DefaultStaticContentLoader` + maps request paths to classpath roots and where to hook the `/webjars` prefix — + exact class/method names must be confirmed in the framework session before coding. + +### 1.3 URL-building API (the cooperation surface) + +- **`WebJarUrlProvider`** interface (public API, stable): + - Resolves a logical webjar path to a servable URL, prefixed with the app + context + `staticContentPath` + `/webjars` + resolved version + path. + - Injectable/obtainable via the Struts container so plugins depend on the + interface, not internals. +- **`` tag** and **`<@s.webjar path="…"/>` FreeMarker macro**: + - Thin wrappers over `WebJarUrlProvider`. + - Emit only the resolved URL string (so callers embed it in `` / `