Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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"
}
6 changes: 6 additions & 0 deletions bindings/devup-ui-wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
mod reservation;
pub use reservation::{
ReservationInput, get_unreserved_class_slot_count, reserve_class_slots_internal,
};

use css::class_map::{set_class_map, with_class_map};
use css::file_map::{
canonical, is_global, set_canonical_map, set_file_map, with_canonical_map, with_file_map,
Expand Down Expand Up @@ -332,6 +337,7 @@ pub fn seed_file_map(files: Vec<String>) {
/// and the module resolver, so the next build starts from its own options
/// alone. Theme, shorthands and debug mode are set by every build, and stay.
pub fn reset_build_state_internal() {
css::slot_reservation::reset();
css::class_map::reset_class_map();
css::file_map::reset_file_map();
css::file_map::reset_canonical_map();
Expand Down
129 changes: 129 additions & 0 deletions bindings/devup-ui-wasm/src/reservation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Rule-free reservation through the real extractor and sheet allocation path.

use bimap::BiHashMap;
use css::{file_map, slot_reservation::ScratchSlots};
use extractor::reservation_state::ScratchExtractionState;
use extractor::{ExtractOption, ModuleResolver, extract_with_modules, extract_without_source_map};
use sheet::StyleSheet;
use wasm_bindgen::prelude::*;

use crate::with_style_sheet_mut;
mod rows;
#[cfg(not(tarpaulin_include))]
use crate::{MODULE_RESOLVER, call_module_resolver, import_aliases_from_js, js_error};

/// Prepared physical input, keyed exactly as its later real transform.
pub struct ReservationInput {
pub filename: String,
pub code: String,
pub css_dir: String,
}

struct ScratchSheet {
live: StyleSheet,
files: BiHashMap<String, usize>,
}

impl Default for ScratchSheet {
fn default() -> Self {
let files = file_map::get_file_map();
let live = with_style_sheet_mut(|sheet| {
let mut live = std::mem::take(sheet);
sheet.theme = std::mem::take(&mut live.theme);
live
});
Self { live, files }
}
}

impl Drop for ScratchSheet {
fn drop(&mut self) {
with_style_sheet_mut(|sheet| {
std::mem::swap(&mut self.live.theme, &mut sheet.theme);
*sheet = std::mem::take(&mut self.live);
});
file_map::set_file_map(std::mem::take(&mut self.files));
}
}

/// Reserve in supplied physical-path order; suppress per-input errors, emitting nothing.
pub fn reserve_class_slots_internal(
inputs: &[ReservationInput],
option: ExtractOption,
resolver: Option<&ModuleResolver>,
) {
let slots = ScratchSlots::default();
let files: Vec<String> = inputs.iter().map(|input| input.filename.clone()).collect();
file_map::seed_file_numbers(&files);
for input in inputs {
let _ = file_map::get_file_num_by_filename(&input.filename);
}
let _sheet = ScratchSheet::default();
let _extraction = ScratchExtractionState::default();
for input in inputs {
let file_option = ExtractOption {
css_dir: input.css_dir.clone(),
..option.clone()
};
let extracted = match resolver {
Some(resolver) => {
extract_with_modules(&input.filename, &input.code, file_option, false, resolver)
}
None => extract_without_source_map(&input.filename, &input.code, file_option),
};
if let Ok(output) = extracted {
let bucket = file_map::canonical(&input.filename);
let global = option.single_css || file_map::is_global(&input.filename);
with_style_sheet_mut(|sheet| {
sheet.rm_global_css(&input.filename, global);
sheet.update_styles(&output.styles, &bucket, global);
});
}
}
slots.commit();
}

/// Reserve `[filename, code, cssDir]` rows using the current resolver and configuration.
#[wasm_bindgen(js_name = "reserveClassSlots")]
#[cfg(not(tarpaulin_include))]
pub fn reserve_class_slots(
rows: JsValue,
package: &str,
single_css: bool,
import_main_css_in_code: bool,
_import_main_css_in_css: bool,
import_aliases: JsValue,
) -> Result<(), JsValue> {
let rows: Vec<Vec<String>> = serde_wasm_bindgen::from_value(rows).map_err(js_error)?;
let inputs = rows::parse_rows(rows).map_err(js_error)?;
let option = ExtractOption {
package: package.to_string(),
single_css,
import_main_css: import_main_css_in_code,
import_aliases: import_aliases_from_js(import_aliases)?,
..ExtractOption::default()
};
let resolver = MODULE_RESOLVER.with_borrow(Clone::clone).map(|resolver| {
move |specifier: &str, importer: &str| call_module_resolver(&resolver, specifier, importer)
});
reserve_class_slots_internal(
&inputs,
option,
resolver
.as_ref()
.map(|resolver| resolver as &ModuleResolver),
);
Ok(())
}

/// New live class and file slots since this build's first reservation; repeats do not count.
#[must_use]
#[wasm_bindgen(js_name = "getUnreservedClassSlotCount")]
pub fn get_unreserved_class_slot_count() -> usize {
css::slot_reservation::unreserved_slot_count()
}

#[cfg(test)]
mod file_slots_tests;
#[cfg(test)]
mod tests;
Loading
Loading