diff --git a/CHANGELOG.md b/CHANGELOG.md index a22695f..abb7a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Per-release notes are also published on each [GitHub Release](https://github.com - `typescript::rewrite_str` and a public `RewriteOptions`: apply an output policy (minify, comments, inline source map) to plain JavaScript through the transformer-free rewrite pass the build already uses internally. Consumers no longer route generated or copied JS through `compile_str_with`, whose Lit-preset transform may alter hand-written semantics. +- `PackageSpec::keep_tagged`: a keep-filter with a tag that joins the extraction cache key. + A `fn` pointer has no stable identity, so a tagless filter reuses a cached tree even after the filter's shape changed; the tag makes a filter change re-extract, and a tree cached without one re-extracts once on adoption. - `examples/bundle`: a pure-frontend demo of `--bundle`, configured from the `web_modules` block in `package.json`. - The `embedded` example bakes source maps and collects legal comments; the gh-pages CI dogfood covers `--comments collect` and `--no-minify-web-modules`. - `--bundle` and `--bundle-entry ` (package.json `"bundle": {"entries": [...]}`, builder `Build::bundle` / `Build::bundle_entry`, action inputs `bundle`/`bundle-entries`) fold the built tree per entry point, inside the atomic build. diff --git a/src/core/vendor.rs b/src/core/vendor.rs index a0db628..9b56c9b 100644 --- a/src/core/vendor.rs +++ b/src/core/vendor.rs @@ -119,6 +119,9 @@ pub struct PackageSpec { /// Compile the package's TypeScript after extraction, so what lands in the /// vendor tree is what a browser loads. Set by [`PackageSpec::as_source`]. compile: bool, + /// Names the keep-filter's shape in the extraction cache key. Set by + /// [`PackageSpec::keep_tagged`]. + extract_tag: Option, } impl PackageSpec { @@ -136,6 +139,7 @@ impl PackageSpec { extract: Extract::BrowserAssets, imports: Imports::Auto, compile: false, + extract_tag: None, } } @@ -157,6 +161,7 @@ impl PackageSpec { extract: Extract::BrowserAssets, imports: Imports::None, compile: false, + extract_tag: None, } } @@ -177,6 +182,7 @@ impl PackageSpec { extract: Extract::BrowserAssets, imports: Imports::Auto, compile: false, + extract_tag: None, } } @@ -212,11 +218,23 @@ impl PackageSpec { } /// Shorthand for `.extract(Extract::Filter(keep))`. + /// + /// A `fn` pointer has no stable identity, so a tagless filter reuses a + /// cached tree even after the filter's shape changed; use + /// [`keep_tagged`](Self::keep_tagged) to make a filter change re-extract. pub fn keep(mut self, keep: fn(&str) -> Option) -> Self { self.extract = Extract::Filter(keep); self } + /// Like [`keep`](Self::keep), with a tag naming the filter's shape in the + /// extraction cache key. Changing the tag re-extracts the package; a tree + /// cached without a tag re-extracts once on adoption. + pub fn keep_tagged(mut self, tag: impl Into, keep: fn(&str) -> Option) -> Self { + self.extract_tag = Some(tag.into()); + self.keep(keep) + } + /// Take the package's **sources** and compile them, for a package that publishes /// no browser-usable build. /// @@ -1161,6 +1179,15 @@ fn compiled_key(key: String, compile: bool) -> String { } } +/// Fold the keep-filter tag into the cache key, like [`compiled_key`] folds +/// the compile schema. +fn tagged_key(key: String, extract_tag: Option<&str>) -> String { + match extract_tag { + Some(tag) => format!("{key}+keep:{tag}"), + None => key, + } +} + /// Whether a git reference is a commit id, and so cannot move. /// /// A branch or tag can be repointed at any time, which is what makes keying a cache on @@ -1258,7 +1285,8 @@ fn vendor_inner( // A known key can settle freshness without the network. A mutable reference // cannot, so it always fetches and then compares the archive's contents. - let keyed = |key: String| compiled_key(key, spec.compile); + let keyed = + |key: String| tagged_key(compiled_key(key, spec.compile), spec.extract_tag.as_deref()); let cache_key = cache_key.map(keyed); let fresh = |key: &Option| { key.as_deref() @@ -1925,6 +1953,54 @@ mod tests { ); } + #[test] + fn keep_tag_joins_the_cache_key() { + fn only_js(rel: &str) -> Option { + rel.ends_with(".js").then(|| rel.to_string()) + } + let tagless = PackageSpec::npm("lit", "^3").keep(only_js); + let tagged = PackageSpec::npm("lit", "^3").keep_tagged("js", only_js); + assert_eq!( + tagged_key("3.1.0".into(), tagless.extract_tag.as_deref()), + "3.1.0" + ); + assert_eq!( + tagged_key("3.1.0".into(), tagged.extract_tag.as_deref()), + "3.1.0+keep:js" + ); + } + + #[test] + fn keep_tag_change_invalidates_the_marker() { + let tmp = tempfile::tempdir().unwrap(); + let marker = tmp.path().join(".lit.version"); + let dest = tmp.path().join("lit"); + std::fs::create_dir_all(&dest).unwrap(); + std::fs::write(dest.join("index.js"), "export {};").unwrap(); + + let extract = Extract::BrowserAssets; + cache::write_marker(&marker, &tagged_key("3.1.0".into(), Some("js+maps"))).unwrap(); + assert!(is_up_to_date( + &marker, + &tagged_key("3.1.0".into(), Some("js+maps")), + &dest, + &extract + )); + // A different tag re-extracts; so does dropping or adopting one. + assert!(!is_up_to_date( + &marker, + &tagged_key("3.1.0".into(), Some("js")), + &dest, + &extract + )); + assert!(!is_up_to_date( + &marker, + &tagged_key("3.1.0".into(), None), + &dest, + &extract + )); + } + #[test] fn git_spec_defaults() { let spec = PackageSpec::git("feathericons/feather", "v4.29.2");