Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
8e31bbc
fix(extractor): stabilize environment names and authored evaluation
owjs3901 Oct 5, 2026
1968ddf
fix(css): bound unnumbered source names with exact collision claims
owjs3901 Oct 6, 2026
ce6296e
fix(naming): bind scoped static atoms and transactional cache restora…
owjs3901 Oct 6, 2026
a9c804a
fix(extractor): preserve dynamic typography class selection and sourc…
owjs3901 Oct 6, 2026
135d0aa
fix(extractor): preserve styled creation and qualify naming coverage
owjs3901 Oct 7, 2026
f317421
fix(extractor): reject unsupported argument spreads and cover residua…
owjs3901 Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_argument_spread_contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Keep JavaScript argument-spread semantics in css() and styled(): compose supported literal arrays statically, and reject strings, other iterables and unknown spreads with a located composition error instructing users to pass the value itself instead. No iterators execute at build time or at styling runtime. Preserve optional producer coordinates and checked prelude offsets for located collision diagnostics.",
"date": "2026-10-07T00:00:00.000Z"
}
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_bun_plugin_build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/bun-plugin/package.json": "Patch"
},
"note": "Bun plugin works with Bun.build: `DevupUI()` (from `@devup-ui/bun-plugin/register`) serves the generated stylesheet to the bundler's CSS loader once every other module is loaded, so the build emits a CSS output with the styles of the whole bundle instead of an empty module; class names are short there and readable under the runtime (`debug` option). Files importing the packages Devup UI takes the place of (`@emotion/react`, `@emotion/styled`, `styled-components`, `@vanilla-extract/css`), `@stylexjs/stylex`, or a subpath or re-export of these or `@devup-ui/react`, are compiled instead of skipped",
"date": "2026-09-30T00: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"
}
9 changes: 9 additions & 0 deletions .changepacks/changepack_log_css_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": {
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch"
},
"note": "Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside `src`) were missing from the output. The files the entries reach under `src` are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles",
"date": "2026-09-30T00:00:00.000Z"
}
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"
}
12 changes: 12 additions & 0 deletions .changepacks/changepack_log_environment_stable_names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch",
"packages/components/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch"
},
"note": "Stabilize class and runtime-variable names across build environments, bound unnumbered source identities with compact deterministic names, reset generated variables on their owning elements, and preserve authored evaluation order and lazy selection. Exact content-name and source-name collisions report both contents and their best real source locations; conflicting source-variable roles and unprotected naming caches now fail closed without rejecting literal CSS content strings.",
"date": "2026-10-05T00:00:00.000Z"
}
13 changes: 13 additions & 0 deletions .changepacks/changepack_log_static_scoped_names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"changes": {
"bindings/devup-ui-wasm/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",
"packages/components/package.json": "Patch"
},
"note": "Compile proven static responsive arrays directly to classes and isolate content-named atoms by their existing delivered sheet identity. Safe original sources keep independent D9 counter namespaces even when their CSS shares a collapsed bucket. Build adapters supply an independent names-only project root; resolver IDs, D9 numbering and stylesheet placement stay unchanged. Versioned self-contained naming snapshots ignore and rebuild obsolete or invalid cache data that reaches WASM; cache build errors are reserved for real exact name collisions with located diagnostics. Static Textarea font-size arrays retain their declarations and DOM while compiling directly to classes.",
"date": "2026-10-06T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_creation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Evaluate supported conditional styled rule arguments once when the component is created, preserving branch laziness, authored field order, static selectors and layers, typography and runtime value associations. Keep rule capture outside attrs wrappers while authored attrs callbacks remain render-time. Responsive typography retains its shared base class and order-aware class selection without introducing empty styles for proven inert base-preset lookups. This scheduling repair adds no build errors or styling runtime.",
"date": "2026-10-07T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_typography_classes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Select dynamic typography and enum classes from the authored value exactly once in source order, without typography CSS variables or declarations. Preserve responsive and selector-scoped presets, conditional style order, template coercion and the existing spread contract. No new build errors or styling runtime are introduced.",
"date": "2026-10-07T00: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