Skip to content

Commit b9ce5f3

Browse files
committed
Emit versioned asset bundles for immutable caching
1 parent ed59f1c commit b9ce5f3

7 files changed

Lines changed: 3915 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
run: |
3636
make clean
3737
make assets
38-
git diff --exit-code static/sf/sf.css static/sf/sf.js
38+
git diff --exit-code static/sf/sf.css static/sf/sf.js static/sf/sf.*.css static/sf/sf.*.js
3939
4040
- name: Check formatting
4141
run: cargo fmt --all -- --check

.versionrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
"filename": "Cargo.toml",
1111
"updater": "scripts/cargo-version.js"
1212
}
13-
]
13+
],
14+
"scripts": {
15+
"postbump": "make clean assets && git add -A static/sf"
16+
}
1417
}

Makefile

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ RUST_VERSION := 1.75+
2323
# ============== Asset Sources ==============
2424
CSS_SRC := $(sort $(wildcard css-src/*.css))
2525
JS_SRC := $(sort $(wildcard js-src/*.js))
26+
VERSIONED_CSS := static/sf/sf.$(VERSION).css
27+
VERSIONED_JS := static/sf/sf.$(VERSION).js
2628

2729
# ============== Phony Targets ==============
2830
.PHONY: banner help assets build build-release test test-quick test-doc test-unit test-one \
@@ -40,16 +42,18 @@ banner:
4042

4143
# ============== Asset Targets ==============
4244

43-
assets: static/sf/sf.css static/sf/sf.js
45+
assets: static/sf/sf.css static/sf/sf.js $(VERSIONED_CSS) $(VERSIONED_JS)
4446

45-
static/sf/sf.css: $(CSS_SRC)
47+
static/sf/sf.css $(VERSIONED_CSS): $(CSS_SRC)
4648
@printf "$(PROGRESS) CSS sf.css ($(words $(CSS_SRC)) files)\n"
47-
@cat $(CSS_SRC) > $@
49+
@cat $(CSS_SRC) > static/sf/sf.css
50+
@cp static/sf/sf.css $(VERSIONED_CSS)
4851
@printf "$(GREEN)$(CHECK) CSS bundled$(RESET)\n"
4952

50-
static/sf/sf.js: $(JS_SRC)
53+
static/sf/sf.js $(VERSIONED_JS): $(JS_SRC)
5154
@printf "$(PROGRESS) JS sf.js ($(words $(JS_SRC)) files)\n"
52-
@cat $(JS_SRC) > $@
55+
@cat $(JS_SRC) > static/sf/sf.js
56+
@cp static/sf/sf.js $(VERSIONED_JS)
5357
@printf "$(GREEN)$(CHECK) JS bundled$(RESET)\n"
5458

5559
# ============== Build Targets ==============
@@ -238,7 +242,7 @@ publish: banner
238242
clean:
239243
@printf "$(ARROW) Cleaning build artifacts...\n"
240244
@cargo clean
241-
@rm -f static/sf/sf.css static/sf/sf.js
245+
@rm -f static/sf/sf.css static/sf/sf.js static/sf/sf.*.css static/sf/sf.*.js
242246
@printf "$(GREEN)$(CHECK) Clean complete$(RESET)\n"
243247

244248
# ============== Development ==============

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ This repository keeps both shipped UI code and design exploration in the same tr
3232
- Planned or exploratory ideas may appear in CSS or wireframes before the public API is finished. Those should not be treated as supported integration surface until they are wired into a shipped asset and described in the README API reference.
3333
- When adding new surface area, update the JavaScript API, README, and runnable examples in the same change so the public contract stays explicit.
3434

35+
For production caching, versioned bundle filenames are also emitted as
36+
`/sf/sf.<crate-version>.css` and `/sf/sf.<crate-version>.js`. Those versioned
37+
files are served with immutable caching, while the stable `sf.css` and `sf.js`
38+
paths remain available for compatibility.
39+
3540
## Screenshots
3641

3742
**Planner123** — Gantt chart with split panes, project-colored bars, and constraint scoring:
@@ -506,6 +511,10 @@ Use `make package-verify` to inspect the exact crate contents that would be publ
506511

507512
The verification step checks that required bundled assets and crate metadata are present, and that development-only sources such as `css-src/`, `js-src/`, `scripts/`, and screenshots are not shipped in the published crate.
508513

514+
Bundling writes both stable compatibility assets (`static/sf/sf.css`,
515+
`static/sf/sf.js`) and versioned assets (`static/sf/sf.<version>.css`,
516+
`static/sf/sf.<version>.js`).
517+
509518
## Acknowledgments
510519

511520
solverforge-ui builds on these excellent open-source projects:

src/lib.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,27 @@ fn mime_from_path(path: &str) -> &'static str {
5353
}
5454

5555
fn is_immutable(path: &str) -> bool {
56-
path.starts_with("fonts/") || path.starts_with("vendor/") || path.starts_with("img/")
56+
path.starts_with("fonts/")
57+
|| path.starts_with("vendor/")
58+
|| path.starts_with("img/")
59+
|| is_versioned_bundle(path)
60+
}
61+
62+
fn is_versioned_bundle(path: &str) -> bool {
63+
path.strip_prefix("sf.")
64+
.and_then(|rest| rest.rsplit_once('.'))
65+
.map(|(version, ext)| {
66+
!version.is_empty()
67+
&& version.chars().all(|ch| {
68+
ch.is_ascii_digit()
69+
|| ch == '.'
70+
|| ch == '-'
71+
|| ch == '+'
72+
|| ch.is_ascii_alphabetic()
73+
})
74+
&& matches!(ext, "css" | "js")
75+
})
76+
.unwrap_or(false)
5777
}
5878

5979
#[cfg(test)]
@@ -65,6 +85,17 @@ mod tests {
6585
};
6686
use tower::util::ServiceExt;
6787

88+
#[test]
89+
fn versioned_bundles_are_detected() {
90+
assert!(is_versioned_bundle("sf.0.1.0.css"));
91+
assert!(is_versioned_bundle("sf.0.1.0.js"));
92+
assert!(is_versioned_bundle("sf.0.2.0-beta.1.js"));
93+
assert!(is_versioned_bundle("sf.0.1.0+build.7.css"));
94+
assert!(!is_versioned_bundle("sf.css"));
95+
assert!(!is_versioned_bundle("sf.js"));
96+
assert!(!is_versioned_bundle("vendor/sf.0.1.0.js"));
97+
}
98+
6899
#[test]
69100
fn caches_paths_are_predicted_correctly() {
70101
assert_eq!(mime_from_path("styles/sf.css"), "text/css; charset=utf-8");
@@ -78,9 +109,20 @@ mod tests {
78109
assert!(is_immutable("fonts/jetbrains-mono.woff2"));
79110
assert!(is_immutable("vendor/leaflet/leaflet.js"));
80111
assert!(is_immutable("img/solverforge-logo.svg"));
112+
assert!(is_immutable("sf.0.1.0.css"));
113+
assert!(is_immutable("sf.0.1.0+build.7.js"));
81114
assert!(!is_immutable("sf.css"));
82115
}
83116

117+
#[test]
118+
fn mime_detection_still_works_for_versioned_assets() {
119+
assert_eq!(mime_from_path("sf.0.1.0.css"), "text/css; charset=utf-8");
120+
assert_eq!(
121+
mime_from_path("sf.0.1.0+build.7.js"),
122+
"application/javascript; charset=utf-8"
123+
);
124+
}
125+
84126
#[tokio::test]
85127
async fn serves_assets_with_expected_headers() {
86128
let app = routes();

0 commit comments

Comments
 (0)