Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b6984e6
fix(bun-plugin): emit the stylesheet in Bun.build and compile alias a…
owjs3901 Sep 30, 2026
71a5984
fix(webpack-plugin,rsbuild-plugin): keep every extracted style in the…
owjs3901 Sep 30, 2026
383ca05
fix(next-plugin): isolate coordinator port file and explain unreachab…
owjs3901 Oct 2, 2026
2314d8f
Merge remote-tracking branch 'origin/fix/coordinator-port-isolation' …
owjs3901 Oct 3, 2026
8ab7c3e
Merge remote-tracking branch 'origin/fix/bun-plugin' into feat/determ…
owjs3901 Oct 3, 2026
1d6a14a
feat(plugins): number files and name classes independently of process…
owjs3901 Oct 3, 2026
1fc5744
chore: remove files test runs left behind
owjs3901 Oct 3, 2026
ffd7dc7
fix(plugins): number buckets and extracted files only, leave vite css…
owjs3901 Oct 3, 2026
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
4f1f953
chore: merge main into deterministic numbering
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
01485c1
Merge main b711ed1f into feat/deterministic-numbering
owjs3901 Oct 8, 2026
53d95c2
Merge main 9a953607 into feat/deterministic-numbering
owjs3901 Oct 8, 2026
143612f
Merge main db631c6c into feat/deterministic-numbering
owjs3901 Oct 8, 2026
43a8c7b
Merge main 11790bef into feat/deterministic-numbering
owjs3901 Oct 8, 2026
84af97a
Merge updated #743 into fix/atom-hoist-names
owjs3901 Oct 8, 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
10 changes: 10 additions & 0 deletions .changepacks/changepack_log_atom_hoist_names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"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"
},
"note": "Make atomHoist class names deterministic and match generated references to emitted CSS, freeze hoisting from predeclared route reach, and preserve dynamic !important, keyframes, and first-value responsive-token identities. No new build errors.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_coordinator_port_isolation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/next-plugin/package.json": "Patch"
},
"note": "Isolate the coordinator port file per owning process when another live next dev/build holds it, record the owner pid, and report port, pid and recovery when the coordinator is missing or unreachable",
"date": "2026-10-02T12:41:12.3693972Z"
}
13 changes: 13 additions & 0 deletions .changepacks/changepack_log_deterministic_numbering.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch"
},
"note": "Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale",
"date": "2026-10-01T00:00:00.000Z"
}
244 changes: 244 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,244 @@
//! 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 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))
}
30 changes: 30 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,30 @@
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, "Parser panicked");
assert_eq!(error.to_string(), "src/shared/s00.tsx: Parser panicked");
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