Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a5cd7e5
fix(wasm): align deterministic atom names and persisted hoisting
owjs3901 Oct 4, 2026
818de80
test(next-plugin): verify real atom hoist loaders across builds
owjs3901 Oct 4, 2026
8689baa
test(rsbuild-plugin): verify real atom hoist hooks across builds
owjs3901 Oct 4, 2026
d859fb4
test(vite-plugin): verify real atom hoist transforms across builds
owjs3901 Oct 4, 2026
1999dde
docs(plugins): explain frozen canonical bucket hoisting
owjs3901 Oct 4, 2026
ada56b5
chore(changepack): patch atom hoist naming and plugin contracts
owjs3901 Oct 4, 2026
f3ef138
Merge remote-tracking branch 'origin/feat/deterministic-numbering' in…
owjs3901 Oct 4, 2026
13ccaea
test(vite-plugin): follow array and restore plugin contract
owjs3901 Oct 4, 2026
aaab756
test(css): cover route unions and restored mixed atom placement
owjs3901 Oct 4, 2026
84af97a
Merge updated #743 into fix/atom-hoist-names
owjs3901 Oct 8, 2026
d5750cd
chore: merge updated deterministic numbering into atom hoist names
owjs3901 Oct 14, 2026
7b3d125
fix(plugins): reserve shared counter slots before extraction
owjs3901 Oct 15, 2026
fbb1e23
fix(plugin-utils): isolate invalid manifest shapes during reservation
owjs3901 Oct 15, 2026
2ed8de6
fix(plugin-utils): ignore quoted import snippets during discovery
owjs3901 Oct 15, 2026
f1f66ca
fix(plugin-utils): reserve literal CommonJS dependency closures
owjs3901 Oct 15, 2026
bf800ab
fix(rsbuild): reserve current final compiler entries
owjs3901 Oct 15, 2026
7f27eeb
fix(plugin-utils): refresh slots after resolution-only changes
owjs3901 Oct 15, 2026
4106aab
chore: merge main into order-independent class slots
owjs3901 Oct 15, 2026
af54ab8
chore: merge Next webpack JSX and source map prerequisite
owjs3901 Oct 15, 2026
d7b7de6
chore: merge main into atom hoist naming rework
owjs3901 Oct 16, 2026
9f2ad99
fix(webpack): reconcile watch CSS within each generation
owjs3901 Oct 16, 2026
7bd8150
chore: merge reserved counter slots into atom hoist rework
owjs3901 Oct 16, 2026
59ef63b
chore: merge Next webpack CSS delivery prerequisite
owjs3901 Oct 16, 2026
b019767
test(next): qualify shared slots across native webpack modes
owjs3901 Oct 16, 2026
d4e9b35
fix(wasm): allocate hoisted atoms from reserved short counters
owjs3901 Oct 16, 2026
2eccc50
fix(plugins): enable route atom hoisting by default
owjs3901 Oct 16, 2026
046b2a9
fix(plugins): defer atom hoisting without reserved shared slots
owjs3901 Oct 16, 2026
b2ce080
chore: merge final shared counter slot prerequisites
owjs3901 Oct 16, 2026
6d7937b
fix(extractor): merge main into deterministic class slots
owjs3901 Oct 17, 2026
74238b2
fix(sheet): merge deterministic slots and preserve global order
owjs3901 Oct 17, 2026
95c5b74
fix(wasm): merge main and cover reservation ABI
owjs3901 Oct 18, 2026
01d4fd1
fix(wasm): merge latest main into class reservations
owjs3901 Oct 18, 2026
513c19b
fix(sheet): merge updated class reservations and latest main
owjs3901 Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changepacks/changepack_log_atom_hoist_names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch"
},
"note": "Enable atomHoist by default at two routes in Webpack, Rsbuild and Next Webpack, with numeric zero opting out and no effect in singleCss mode. Keep Vite and Next Turbopack in per-file mode until their shared-slot reservation lands, warning once for explicit positive atomHoist requests. Allocate shared and file-local short counter names through deterministic reserved slots instead of content names; preserve frozen route plans, dynamic !important, local keyframes, first-value responsive tokens, shared CSS updates and persisted placement. Bump the naming cache generation. No new build errors.",
"date": "2026-10-01T00:00:00.000Z"
}
12 changes: 12 additions & 0 deletions .changepacks/changepack_log_order_independent_shared_slots.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch"
},
"note": "Reserve existing short counter class, variable, keyframe and raw-file slots in physical input order before Bun, webpack, Rsbuild and Next webpack extraction. Watch revisions append sorted slots without renumbering. No new source build errors or styling runtime; unknown virtual, generated and opaque outside-inventory inputs retain observable residual allocation.",
"date": "2026-10-15T00:00:00.000Z"
}
254 changes: 254 additions & 0 deletions bindings/devup-ui-wasm/src/atom_hoist_measurement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//! Test-only measurements under immutable, predeclared module reach.

mod error_tests;
mod report;
mod snapshot;

use snapshot::Snapshot;

use super::{
code_extract_internal, register_theme_internal, reset_build_state_internal, seed_file_map,
set_atom_hoist, with_style_sheet,
};
use std::collections::{HashMap, HashSet};

const ROUTE_IDS: [u32; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
const ROUTES: usize = ROUTE_IDS.len();
const SHARED_MODULES: usize = 32;
const SHARED_ATOMS: usize = 80;
const PRIVATE_ATOMS: usize = 25;
const PROPS: [(&str, &str); 8] = [
("w", "width"),
("h", "height"),
("p", "padding"),
("m", "margin"),
("minW", "min-width"),
("minH", "min-height"),
("maxW", "max-width"),
("maxH", "max-height"),
];

struct Module {
path: String,
atoms: Vec<Atom>,
}

struct Atom {
property: (&'static str, &'static str),
value: String,
}

impl Module {
fn source(&self) -> String {
let mut elements = String::new();
for atom in &self.atoms {
elements.push_str("<Box ");
elements.push_str(atom.property.0);
elements.push_str("=\"");
elements.push_str(&atom.value);
elements.push_str("\" />");
}
format!("import {{ Box }} from '@devup-ui/react'; const x = <>{elements}</>;")
}

fn declarations(&self) -> Vec<String> {
self.atoms
.iter()
.map(|atom| format!("{}:{}", atom.property.1, atom.value))
.collect()
}
}

fn fixture() -> Vec<Module> {
let atoms = |count, start| {
(0..count)
.map(|i| Atom {
property: PROPS[i % PROPS.len()],
value: format!("{}px", start + i),
})
.collect()
};
let mut modules: Vec<Module> = (0..SHARED_MODULES)
.map(|i| Module {
path: format!("src/shared/s{i:02}.tsx"),
atoms: atoms(SHARED_ATOMS, 100_000),
})
.collect();
modules.extend((0..ROUTES).map(|route| Module {
path: format!("src/routes/r{route}.tsx"),
atoms: atoms(PRIVATE_ATOMS, 1_000_000 + route * PRIVATE_ATOMS),
}));
modules.push(Module {
path: "src/unknown.tsx".to_string(),
atoms: atoms(1, 2_000_000),
});
modules
}

#[derive(Debug)]
pub(super) struct ExtractionError {
file: String,
message: String,
}

impl std::fmt::Display for ExtractionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}: {}", self.file, self.message)
}
}

impl std::error::Error for ExtractionError {}

struct BuildState;

impl Drop for BuildState {
fn drop(&mut self) {
reset_build_state_internal();
}
}

struct Measurement {
before: Snapshot,
after: Snapshot,
}

fn build(modules: &[Module], hoist: bool) -> Result<Snapshot, ExtractionError> {
reset_build_state_internal();
let _state = BuildState;
register_theme_internal(sheet::theme::Theme::default());
seed_file_map(modules.iter().map(|m| m.path.clone()).collect());
let all_routes: HashSet<u32> = ROUTE_IDS.into_iter().collect();
let mut reach: HashMap<String, HashSet<u32>> = modules[..SHARED_MODULES]
.iter()
.map(|m| (m.path.clone(), all_routes.clone()))
.collect();
reach.extend(ROUTE_IDS.iter().enumerate().map(|(r, id)| {
(
modules[SHARED_MODULES + r].path.clone(),
HashSet::from([*id]),
)
}));
super::import_file_routes_internal(reach);
set_atom_hoist(hoist.then_some(2));
let mut inventory: Vec<_> = modules
.iter()
.map(|module| super::ReservationInput {
filename: module.path.clone(),
code: module.source(),
css_dir: "df".into(),
})
.collect();
inventory.sort_by(|a, b| a.filename.cmp(&b.filename));
super::reserve_class_slots_internal(&inventory, extractor::ExtractOption::default(), None);
let code = modules
.iter()
.map(|module| {
code_extract_internal(
&module.path,
&module.source(),
"@devup-ui/react",
"df".to_string(),
false,
false,
true,
HashMap::new(),
)
.map(|output| output.code())
.map_err(|message| ExtractionError {
file: module.path.clone(),
message,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Snapshot::capture(modules, &code))
}

fn measure(hoist: bool) -> Result<Measurement, ExtractionError> {
// Given: every shared module reaches all routes before any extraction.
let mut modules = fixture();
let before = build(&modules, hoist)?;
before.assert_loaded_definitions(&modules);
let copies = if hoist { 1 } else { SHARED_MODULES };
for declaration in modules[0].declarations() {
let needle = format!("{{{declaration}}}");
assert_eq!(
before
.sheets
.iter()
.map(|s| s.matches(&needle).count())
.sum::<usize>(),
copies
);
assert_eq!(
before.sheets[0].matches(&needle).count(),
usize::from(hoist)
);
}
// When: deploy a fresh complete build after changing one upstream constant.
for module in &mut modules[..SHARED_MODULES] {
module.atoms[0].value = "900000px".to_string();
}
let after = build(&modules, hoist)?;
// Then: fresh transformed code still loads the exact new declarations.
after.assert_loaded_definitions(&modules);
assert_eq!(after.changed(&before).0, copies);
assert_eq!(after.sheets[0] != before.sheets[0], hoist);
assert_eq!(
after
.sheets
.iter()
.map(|s| s.matches("{width:900000px}").count())
.sum::<usize>(),
copies
);
assert_eq!(
after
.sheets
.iter()
.map(|s| s.matches("{width:100000px}").count())
.sum::<usize>(),
0
);
for (i, module) in modules.iter().enumerate().skip(SHARED_MODULES) {
for declaration in module.declarations() {
assert!(after.sheets[i + 1].contains(&format!("{{{declaration}}}")));
assert!(!after.sheets[0].contains(&format!("{{{declaration}}}")));
}
}
Ok(Measurement { before, after })
}

fn checked_measurements() -> Result<(Measurement, Measurement), ExtractionError> {
let baseline = measure(false)?;
let atom = measure(true)?;
let redundant_payload = (SHARED_MODULES - 1)
* fixture()[0]
.declarations()
.iter()
.map(String::len)
.sum::<usize>();
// Retain at least one route's share of eliminated declaration payload after
// paying the ACTUAL lossless-name/header/import costs. No fixed percentage.
assert!(baseline.before.raw_bytes() >= atom.before.raw_bytes() + redundant_payload / ROUTES);
let baseline_changed = baseline.after.changed(&baseline.before);
let atom_changed = atom.after.changed(&atom.before);
assert_eq!(baseline_changed.0 - atom_changed.0, SHARED_MODULES - 1);
assert!(baseline_changed.1 >= atom_changed.1 + (SHARED_MODULES - 1) * "width:900000px".len());
Ok((baseline, atom))
}

/// Callable replacement body; the caller must retain its serial-test guard.
pub(super) fn assert_predeclared_shared_savings() -> Result<(), ExtractionError> {
let artifacts = build_predeclared_measurement_artifacts()?;
report::assert_artifacts(&artifacts);
println!("{}", artifacts["report"]);
Ok(())
}

/// Build complete artifacts independently of the caller's stdout/env policy.
/// The caller must retain its serial-test guard.
pub(super) fn build_predeclared_measurement_artifacts() -> Result<serde_json::Value, ExtractionError>
{
let (baseline, atom) = checked_measurements()?;
Ok(report::artifacts(&baseline, &atom))
}
33 changes: 33 additions & 0 deletions bindings/devup-ui-wasm/src/atom_hoist_measurement/error_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use super::{ExtractionError, build, fixture};
use serial_test::serial;

#[test]
#[serial]
fn build_reports_failing_file_when_fixture_contains_malformed_jsx() {
// Given: a malformed first atom in the otherwise unchanged real fixture.
let mut modules = fixture();
modules[0].atoms[0].value = "\"".to_string();
// When: extraction fails through the measurement build boundary.
let error: ExtractionError = match build(&modules, true) {
Ok(_) => panic!("malformed JSX must fail extraction"),
Err(error) => error,
};
// Then: the typed error retains its failing file and parser context.
assert_eq!(error.file, "src/shared/s00.tsx");
assert_eq!(error.message, "src/shared/s00.tsx:1:61: Unexpected token");
assert_eq!(
error.to_string(),
"src/shared/s00.tsx: src/shared/s00.tsx:1:61: Unexpected token"
);
assert_eq!(css::atom_hoist::atom_hoist_threshold(), None);
assert_eq!(css::file_routes::get_file_routes().len(), 0);

// Given: a valid fixture after the failed build has released its state.
let valid = fixture();
// When: a subsequent build extracts that fixture without hoisting.
let snapshot = build(&valid, false).unwrap_or_else(|error| panic!("{error}"));
// Then: every emitted class loads the valid fixture's exact declaration.
snapshot.assert_loaded_definitions(&valid);
assert!(!snapshot.sheets[0].contains("{width:100000px}"));
assert!(snapshot.sheets[1].contains("{width:100000px}"));
}
Loading
Loading