Skip to content

Commit bcd240e

Browse files
merge: refresh PR #707 with main and cover inherited render paths
Refs #688 #707 #777 #716 #743 Preserve inherited captures and actual TS targets; cover dynamic bases and assertion guards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2 parents fa11c07 + 76b1a15 commit bcd240e

73 files changed

Lines changed: 5858 additions & 290 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"changes": {
3+
"bindings/devup-ui-wasm/package.json": "Patch",
4+
"packages/plugin-utils/package.json": "Patch",
5+
"packages/vite-plugin/package.json": "Patch",
6+
"packages/webpack-plugin/package.json": "Patch",
7+
"packages/rsbuild-plugin/package.json": "Patch",
8+
"packages/next-plugin/package.json": "Patch",
9+
"packages/bun-plugin/package.json": "Patch"
10+
},
11+
"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",
12+
"date": "2026-10-01T00:00:00.000Z"
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"changes": {
3+
"packages/plugin-utils/package.json": "Patch",
4+
"packages/react/package.json": "Patch",
5+
"bindings/devup-ui-wasm/package.json": "Patch"
6+
},
7+
"note": "@emotion/css is aliased by default: css and keyframes compile like Devup UI's own (numbers in px, as Emotion reads them), injectGlobal declares global CSS. cx takes class names, as in Emotion: strings and runtime values are classes kept as written (falsy values are skipped), an object is a { name: condition } class map, arrays nest, and inline css() calls and css() classes compose with the rules css(a, b) uses. merge(className) splits what the build can read (string tokens, template literal parts) and composes the css() classes in it, keeping a runtime string it cannot split as it is. Namespace members, constant-string bracket access, const destructuring and const alias chains compile through these same named APIs with scope-aware binding resolution; numeric values retain Emotion units after constant inlining and build-time evaluation. The Emotion import disappears when no runtime-only member is used. Ambient declarations type the styling imports (cx(...args: ClassNamesArg[]), merge(className: string)). New located build errors: cx() cannot use a spread, a getter, a setter or a method in a class map, or a template literal call; merge() takes exactly one class string, not a spread or a template literal call; the root module has no default export, namespace member keys must be constant strings naming supported exports, and styling functions cannot be passed, mutated or re-exported. Optional styling reads/calls, namespace rest destructuring, styling defaults or nested patterns, non-const aliases, exported styling bindings and reads whose initialization order cannot be established are build errors instead of runtime dependencies. Styling loaded through require, import-equals or dynamic import must use static ES module imports; an unawaited dynamic namespace that the build cannot follow is also a located error. Its runtime-only APIs (cache, flush, hydrate, sheet, getRegisteredStyles) stay on @emotion/css, including their nested/default destructuring, optional reads and recognized runtime-loader access",
8+
"date": "2026-10-01T00:00:00.000Z"
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"changes": {
3+
"bindings/devup-ui-wasm/package.json": "Patch"
4+
},
5+
"note": "Generate React JSX-runtime calls for styled components in non-JSX source modules while preserving JSX and TSX output. No new build errors.",
6+
"date": "2026-10-13T00:00:00.000Z"
7+
}

‎bindings/devup-ui-wasm/src/lib.rs‎

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,14 @@ pub fn export_sheet() -> Result<String, JsValue> {
243243

244244
/// Internal function to export class map as JSON string (testable without `JsValue`)
245245
pub fn export_class_map_internal() -> Result<String, String> {
246-
with_class_map(serde_json::to_string).map_err(|e| e.to_string())
246+
with_class_map(|map| {
247+
let sorted: BTreeMap<&String, BTreeMap<&String, &usize>> = map
248+
.iter()
249+
.map(|(file, classes)| (file, classes.iter().collect()))
250+
.collect();
251+
serde_json::to_string(&sorted)
252+
})
253+
.map_err(|e| e.to_string())
247254
}
248255

249256
#[wasm_bindgen(js_name = "importClassMap")]
@@ -261,7 +268,11 @@ pub fn export_class_map() -> Result<String, JsValue> {
261268

262269
/// Internal function to export file map as JSON string (testable without `JsValue`)
263270
pub fn export_file_map_internal() -> Result<String, String> {
264-
with_file_map(serde_json::to_string).map_err(|e| e.to_string())
271+
with_file_map(|map| {
272+
let sorted: BTreeMap<&String, &usize> = map.iter().collect();
273+
serde_json::to_string(&sorted)
274+
})
275+
.map_err(|e| e.to_string())
265276
}
266277

267278
#[wasm_bindgen(js_name = "importFileMap")]
@@ -284,7 +295,11 @@ pub fn import_canonical_map_internal(map: HashMap<String, String>) {
284295

285296
/// Internal function to export the canonical map as JSON string (testable without `JsValue`)
286297
pub fn export_canonical_map_internal() -> Result<String, String> {
287-
with_canonical_map(serde_json::to_string).map_err(|e| e.to_string())
298+
with_canonical_map(|map| {
299+
let sorted: BTreeMap<&String, &String> = map.iter().collect();
300+
serde_json::to_string(&sorted)
301+
})
302+
.map_err(|e| e.to_string())
288303
}
289304

290305
#[wasm_bindgen(js_name = "importCanonicalMap")]
@@ -300,6 +315,34 @@ pub fn export_canonical_map() -> Result<String, JsValue> {
300315
export_canonical_map_internal().map_err(js_error)
301316
}
302317

318+
/// Number every file in `files` now, in path order, keeping numbers files
319+
/// already hold, so class prefixes depend on the paths and not on the order
320+
/// workers reach files in.
321+
#[wasm_bindgen(js_name = "seedFileMap")]
322+
pub fn seed_file_map(files: Vec<String>) {
323+
css::file_map::seed_file_numbers(&files);
324+
}
325+
326+
/// Forget everything one build left in the engine.
327+
///
328+
/// That is names, numbers, styles, buckets, routes, the prefix, atom hoisting
329+
/// and the module resolver, so the next build starts from its own options
330+
/// alone. Theme, shorthands and debug mode are set by every build, and stay.
331+
pub fn reset_build_state_internal() {
332+
css::class_map::reset_class_map();
333+
css::file_map::reset_file_map();
334+
css::file_map::reset_canonical_map();
335+
css::file_routes::set_file_routes(HashMap::new());
336+
css::atom_hoist::set_atom_hoist(None);
337+
css::set_prefix(None);
338+
with_style_sheet_mut(|sheet| *sheet = StyleSheet::default());
339+
MODULE_RESOLVER.with_borrow_mut(|current| *current = None);
340+
}
341+
342+
#[wasm_bindgen(js_name = "resetBuildState")]
343+
pub fn reset_build_state() {
344+
reset_build_state_internal();
345+
}
303346
/// Set the atom-level hoist threshold.
304347
///
305348
/// When set to `Some(n)`, a style atom whose content is used by `>= n` distinct
@@ -1929,6 +1972,60 @@ mod tests {
19291972
assert!(json_str.starts_with('{') || json_str.starts_with("[]"));
19301973
}
19311974

1975+
#[test]
1976+
#[serial]
1977+
fn test_exported_maps_are_canonical_json() {
1978+
css::class_map::reset_class_map();
1979+
css::file_map::reset_file_map();
1980+
css::file_map::reset_canonical_map();
1981+
css::class_map::set_class_map(HashMap::from([
1982+
(
1983+
"b.tsx".to_string(),
1984+
HashMap::from([("z".to_string(), 1), ("a".to_string(), 0)]),
1985+
),
1986+
("a.tsx".to_string(), HashMap::from([("k".to_string(), 0)])),
1987+
]));
1988+
assert_eq!(
1989+
export_class_map_internal().unwrap(),
1990+
r#"{"a.tsx":{"k":0},"b.tsx":{"a":0,"z":1}}"#
1991+
);
1992+
seed_file_map(vec!["b.tsx".to_string(), "a.tsx".to_string()]);
1993+
assert_eq!(
1994+
export_file_map_internal().unwrap(),
1995+
r#"{"a.tsx":0,"b.tsx":1}"#
1996+
);
1997+
import_canonical_map_internal(HashMap::from([
1998+
("y".to_string(), "b".to_string()),
1999+
("x".to_string(), "a".to_string()),
2000+
]));
2001+
assert_eq!(
2002+
export_canonical_map_internal().unwrap(),
2003+
r#"{"x":"a","y":"b"}"#
2004+
);
2005+
reset_build_state();
2006+
assert_eq!(export_class_map_internal().unwrap(), "{}");
2007+
assert_eq!(export_file_map_internal().unwrap(), "{}");
2008+
assert_eq!(export_canonical_map_internal().unwrap(), "{}");
2009+
assert_eq!(get_prefix(), None);
2010+
}
2011+
2012+
#[test]
2013+
#[serial]
2014+
fn test_numbers_do_not_depend_on_the_order_files_are_seen() {
2015+
let files = ["src/b.tsx", "src/a.tsx", "src/c.tsx"];
2016+
let mut maps = Vec::new();
2017+
for order in [[0, 1, 2], [2, 1, 0], [1, 2, 0]] {
2018+
reset_build_state_internal();
2019+
seed_file_map(files.iter().map(ToString::to_string).collect());
2020+
for index in order {
2021+
let _ = css::file_map::get_file_num_by_filename(files[index]);
2022+
}
2023+
maps.push(export_file_map_internal().unwrap());
2024+
}
2025+
assert_eq!(maps[0], r#"{"src/a.tsx":0,"src/b.tsx":1,"src/c.tsx":2}"#);
2026+
assert_eq!(maps[0], maps[1]);
2027+
assert_eq!(maps[0], maps[2]);
2028+
}
19322029
#[test]
19332030
#[serial]
19342031
fn test_code_extract_internal_success() {

‎libs/css/src/file_map.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ pub fn get_file_num_by_filename(filename: &str) -> usize {
9393
})
9494
}
9595

96+
/// Give every file in `files` a number now, in path order.
97+
///
98+
/// Numbers files already hold are kept, and a file collapsed into a bucket is numbered as the bucket. The numbers files get then depend on the paths alone,
99+
/// not on which file a worker reaches first; files that appear later (in
100+
/// development) are numbered after the existing ones.
101+
pub fn seed_file_numbers(files: &[String]) {
102+
let mut sorted: Vec<String> = files.iter().map(|file| canonical(file)).collect();
103+
sorted.sort_unstable();
104+
sorted.dedup();
105+
for file in sorted {
106+
let _ = get_file_num_by_filename(&file);
107+
}
108+
}
96109
#[must_use]
97110
pub fn get_filename_by_file_num(file_num: usize) -> String {
98111
with_file_map(|map| {
@@ -196,6 +209,47 @@ mod tests {
196209

197210
use super::*;
198211

212+
#[test]
213+
#[serial]
214+
fn test_seed_file_numbers_numbers_buckets_not_their_members() {
215+
reset_file_map();
216+
set_canonical_map(std::collections::HashMap::from([(
217+
"x.tsx".to_string(),
218+
"a.tsx".to_string(),
219+
)]));
220+
seed_file_numbers(&[
221+
"x.tsx".to_string(),
222+
"b.tsx".to_string(),
223+
"a.tsx".to_string(),
224+
]);
225+
assert_eq!(get_file_num_by_filename("a.tsx"), 0);
226+
assert_eq!(get_file_num_by_filename("b.tsx"), 1);
227+
assert_eq!(get_file_map().len(), 2);
228+
reset_canonical_map();
229+
reset_file_map();
230+
}
231+
232+
#[test]
233+
#[serial]
234+
fn test_seed_file_numbers_is_independent_of_order() {
235+
reset_file_map();
236+
seed_file_numbers(&[
237+
"b.tsx".to_string(),
238+
"a.tsx".to_string(),
239+
"b.tsx".to_string(),
240+
]);
241+
assert_eq!(get_file_num_by_filename("a.tsx"), 0);
242+
assert_eq!(get_file_num_by_filename("b.tsx"), 1);
243+
seed_file_numbers(&[
244+
"c.tsx".to_string(),
245+
"0.tsx".to_string(),
246+
"a.tsx".to_string(),
247+
]);
248+
assert_eq!(get_file_num_by_filename("a.tsx"), 0);
249+
assert_eq!(get_file_num_by_filename("0.tsx"), 2);
250+
assert_eq!(get_file_num_by_filename("c.tsx"), 3);
251+
reset_file_map();
252+
}
199253
#[test]
200254
#[serial]
201255
fn test_set_and_get_file_map() {

0 commit comments

Comments
 (0)