Skip to main content

max / makeover-webview

Name the emitter and the class count in the stylesheet banner A consumer whose lockfile pins an old makeover-webview regenerates a well-formed layout.css with components missing and gets no error anywhere. balanced_breakfast ran on 657 bytes from a 0.1.0 emitter while its manifest asked for 0.5.1, and the only way it surfaced was diffing two apps' generated files. stylesheet() now opens with its own version and the number of classes it ruled, so a stale sheet is visible on sight. VERSION is public because makeover-build and any other emitter wants the same string. The body is assembled before the banner so the count can be read off it; vocabulary::names calls back into stylesheet and would recurse.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-23 21:05 UTC
Signed with PGP, not checked
Commit: 7f2ba781ede335c535418016c56d983a3359d070
Parent: fc3d216
1 file changed, +46 insertions, -8 deletions
M src/lib.rs +46 -8
@@ -401,6 +401,14 @@
401 401 // `tables.css` in its own build.rs from [`list::narrowing_css`], and those
402 402 // rules are as generated as the ones here.
403 403 pub use makeover_geometry::{CSS_LAYER, in_css_layer};
404 +
405 + /// This crate's version, as the generated stylesheet reports it.
406 + ///
407 + /// A consumer whose lockfile still pins an old `makeover-webview` gets a
408 + /// well-formed sheet with components missing and no error anywhere, so the
409 + /// emitter has to name itself in what it writes. Read by
410 + /// `makeover_build::layout_css` through [`stylesheet`].
411 + pub const VERSION: &str = env!("CARGO_PKG_VERSION");
404 412 use makeover_layout::{
405 413 Bevel, CellPart, Depth, Fallback, Fill, Flow, Intent, RowPart, Selector, Sort, State, Token,
406 414 Tone,
@@ -1762,20 +1770,29 @@
1762 1770 /// an at-rule.
1763 1771 #[must_use]
1764 1772 pub fn stylesheet(opts: &Emit) -> String {
1773 + let body = in_css_layer(&format!(
1774 + ":root {{\n{}}}\n\n{}\n{}",
1775 + bevel_properties(opts),
1776 + depth_rules(opts),
1777 + component_rules(opts)
1778 + ));
1779 + // Counted from the body rather than through `vocabulary::names`, which
1780 + // calls back into here.
1781 + let classes = vocabulary::classes_in_css(&body).len();
1782 + let version = VERSION;
1765 1783 format!(
1766 - "/* Generated by makeover-webview from makeover-layout. Do not edit.\n \
1784 + "/* Generated by makeover-webview {version} from makeover-layout, \
1785 + {classes} classes.\n \
1786 + Do not edit. The version and the count are here because a stale\n \
1787 + lockfile fails silently: an older emitter writes a well-formed sheet\n \
1788 + with components missing, and nothing else in the file says so. If\n \
1789 + this version trails what the manifest asks for, re-resolve.\n\n \
1767 1790 Depth is a fill and an edge together; naming them apart is what let\n \
1768 1791 them disagree. See the crate's README and wiki note makeover-layout.\n\n \
1769 1792 Everything below is in the `{CSS_LAYER}` cascade layer. Declare the\n \
1770 1793 order once in your own stylesheet, or this layer's position is decided\n \
1771 1794 by whichever generated file the browser happens to see first:\n\n \
1772 - @layer {CSS_LAYER}, base, components, responsive; */\n{}",
1773 - in_css_layer(&format!(
1774 - ":root {{\n{}}}\n\n{}\n{}",
1775 - bevel_properties(opts),
1776 - depth_rules(opts),
1777 - component_rules(opts)
1778 - ))
1795 + @layer {CSS_LAYER}, base, components, responsive; */\n{body}"
1779 1796 )
1780 1797 }
1781 1798
@@ -2032,6 +2049,27 @@
2032 2049 assert!(css.starts_with("/* Generated by makeover-webview"));
2033 2050 }
2034 2051
2052 + #[test]
2053 + fn the_banner_names_the_emitter_so_a_stale_pin_is_visible_on_sight() {
2054 + // A consumer whose lockfile pins an old version gets a well-formed
2055 + // sheet with components missing and no error. balanced_breakfast ran
2056 + // on 657 bytes from a 0.1.0 emitter while its manifest asked for
2057 + // 0.5.1, and the only way it surfaced was diffing two apps' generated
2058 + // files. The version and the count are what the file says instead.
2059 + let css = stylesheet(&Emit::default());
2060 + let banner = css.lines().next().unwrap();
2061 + assert!(
2062 + banner.contains(VERSION),
2063 + "{banner} does not name the emitter"
2064 + );
2065 + let classes = vocabulary::classes_in_css(&css).len();
2066 + assert!(classes > 0);
2067 + assert!(
2068 + banner.contains(&format!("{classes} classes")),
2069 + "{banner} does not carry the class count"
2070 + );
2071 + }
2072 +
2035 2073 #[test]
2036 2074 fn a_primitive_owns_every_state_it_implies() {
2037 2075 // The whole point of 0.10.0. Anything emitting a hover rule owes the