Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_constant_read_work.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Avoid cloning unreferenced constant initializers and defer initialization analysis until constant reads need it. Extraction output and build errors are unchanged.",
"date": "2026-10-24T00:00:00.000Z"
}
9 changes: 9 additions & 0 deletions .changepacks/changepack_log_js_semantics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/components/package.json": "Patch",
"packages/reset-css/package.json": "Patch"
},
"note": "Keep unknown element-prop spreads wholly as native pass-through data: their keys never select, replace or remove separately authored styles, regardless of order. Preserve fully known ordinary spread composition, canonical aliases, source-ordered once-only values and native className/style merging without spread-key styling variables, fallbacks or conversion code. Preserve exact imported enum and Math values including negative zero, lexical constants, binding-aware CommonJS require, React createElement calls, type-only imports, and compiled reset CSS tests. Retain located errors for intrinsically unreadable explicit styles, runtime styleOrder, unmovable lazy logical suspension, whole Devup UI namespaces escaping to runtime, and immediate lexical temporal-dead-zone reads. Native Emotion css spread restrictions and separate styled css overlap restrictions remain unchanged.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_jsx_call_spread.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Attach extracted classes and authored CSS variables to jsx, jsxs and jsxDEV props wrapped in parentheses, as const or satisfies. Preserve source evaluation order, responsive and conditional styleOrder output, native className/style merging and whole unknown-spread pass-through. Syntax wrappers do not introduce spread replacement diagnostics or a styling runtime.",
"date": "2026-10-12T00:00:00.000Z"
}
35 changes: 32 additions & 3 deletions libs/extractor/src/barrel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ impl Generated {

mod aliases;
mod gate;
mod namespace_aliases;
use aliases::{Reach, declarator_removal, export_edits, is_assigned_from};

fn unreadable(local: &str, code: &str) -> String {
Expand Down Expand Up @@ -792,7 +793,17 @@ impl Rewriter<'_, '_, '_, '_> {
self.replace(namespace, appended, member_span, &name, origin);
}
(None, AstKind::VariableDeclarator(declarator))
if matches!(declarator.id, BindingPattern::BindingIdentifier(_)) => {}
if matches!(declarator.id, BindingPattern::BindingIdentifier(_))
&& declarator
.init
.as_ref()
.is_some_and(|init| init.span() == span)
&& nodes
.parent_kind(nodes.parent_id(node))
.as_variable_declaration()
.is_some_and(|declaration| {
declaration.kind == VariableDeclarationKind::Const
}) => {}
(None, AstKind::VariableDeclarator(declarator))
if declarator
.init
Expand All @@ -804,7 +815,11 @@ impl Rewriter<'_, '_, '_, '_> {
(None, AstKind::ComputedMemberExpression(member)) if member.object.span() == span => {
self.whole_error(local, member.span.start, member.span.end);
}
(None, _) => {}
(None, AstKind::ExportSpecifier(_))
if self.edits.iter().any(|(start, end, _)| {
*start <= span.start as usize && *end >= span.end as usize
}) => {}
(None, _) => self.whole_error(local, span.start, span.end),
}
}

Expand Down Expand Up @@ -1022,6 +1037,7 @@ pub(crate) fn rewrite(
}
}
let wants_semantic = !found.namespaces.is_empty()
|| found.bound.iter().any(|bound| bound.devup.1.is_none())
|| !found.opaque.is_empty()
|| (found
.bound
Expand All @@ -1042,7 +1058,7 @@ pub(crate) fn rewrite(
let locals: FxHashMap<SymbolId, Reach> = found
.bound
.iter()
.filter(|bound| bound.devup.1.as_deref().is_some_and(compiled_export))
.filter(|bound| bound.devup.1.as_deref().is_none_or(compiled_export))
.filter_map(|bound| {
Some((
bound.binding.symbol_id.get()?,
Expand Down Expand Up @@ -1085,6 +1101,19 @@ pub(crate) fn rewrite(
module: declared.module.clone(),
source: declared.source.clone(),
})
.chain(
found
.bound
.iter()
.filter(|bound| bound.devup.1.is_none())
.map(|bound| Namespace {
local: bound.binding.name.to_string(),
symbol: bound.binding.symbol_id.get(),
after: bound.after,
module: None,
source: bound.devup.0.clone(),
}),
)
.collect();
for namespace in &spaces {
rewriter.read_namespace(namespace);
Expand Down
21 changes: 7 additions & 14 deletions libs/extractor/src/barrel/aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,13 @@ impl Rewriter<'_, '_, '_, '_> {
Some((bound.binding.symbol_id.get()?, (bound, bound.devup.clone())))
})
.collect();
let mut spaces: FxHashMap<SymbolId, usize> = namespaces
let first_new = namespaces.len();
self.namespace_aliases(namespaces);
let spaces: FxHashMap<SymbolId, usize> = namespaces
.iter()
.enumerate()
.filter_map(|(index, namespace)| Some((namespace.symbol?, index)))
.collect();
let first_new = namespaces.len();
for node in nodes.iter() {
let AstKind::VariableDeclarator(declarator) = node.kind() else {
continue;
Expand All @@ -249,18 +250,10 @@ impl Rewriter<'_, '_, '_, '_> {
if declaration.kind != VariableDeclarationKind::Const {
continue;
}
if let Some(&index) = spaces.get(&origin) {
let origin = &namespaces[index];
let namespace = Namespace {
local: id.name.to_string(),
symbol: Some(alias),
after: origin.after,
module: origin.module.clone(),
source: origin.source.clone(),
};
spaces.insert(alias, namespaces.len());
namespaces.push(namespace);
} else if let Some((origin, reach)) = named.get(&origin).cloned() {
if spaces.contains_key(&origin) {
continue;
}
if let Some((origin, reach)) = named.get(&origin).cloned() {
named.insert(alias, (origin, reach.clone()));
let exported = matches!(
nodes.parent_kind(declaration_id),
Expand Down
58 changes: 58 additions & 0 deletions libs/extractor/src/barrel/namespace_aliases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Whole namespace aliases, followed by semantic identity regardless of declaration order.

use oxc_ast::AstKind;
use oxc_ast::ast::{BindingPattern, Expression, VariableDeclarationKind};
use rustc_hash::FxHashMap;

use super::{Namespace, Rewriter, reference_symbol};

impl Rewriter<'_, '_, '_, '_> {
/// Collects every immutable namespace alias before checking its runtime reads.
pub(super) fn namespace_aliases(&self, namespaces: &mut Vec<Namespace>) {
let mut spaces: FxHashMap<_, _> = namespaces
.iter()
.enumerate()
.filter_map(|(index, namespace)| Some((namespace.symbol?, index)))
.collect();
let nodes = self.semantic.nodes();
loop {
let before = spaces.len();
for node in nodes.iter() {
let AstKind::VariableDeclarator(declarator) = node.kind() else {
continue;
};
let (BindingPattern::BindingIdentifier(id), Some(Expression::Identifier(init))) =
(&declarator.id, &declarator.init)
else {
continue;
};
let (Some(alias), Some(origin), AstKind::VariableDeclaration(declaration)) = (
id.symbol_id.get(),
reference_symbol(init, self.semantic),
nodes.parent_kind(node.id()),
) else {
continue;
};
if declaration.kind != VariableDeclarationKind::Const || spaces.contains_key(&alias)
{
continue;
}
if let Some(&index) = spaces.get(&origin) {
let origin = &namespaces[index];
let namespace = Namespace {
local: id.name.to_string(),
symbol: Some(alias),
after: origin.after,
module: origin.module.clone(),
source: origin.source.clone(),
};
spaces.insert(alias, namespaces.len());
namespaces.push(namespace);
}
}
if spaces.len() == before {
break;
}
}
}
}
133 changes: 133 additions & 0 deletions libs/extractor/src/barrel/namespace_escape_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use super::{NS, Project, namespace_code, namespace_error};
use crate::barrel::{Barreled, rewrite};
use serial_test::serial;

#[test]
#[serial]
fn runtime_namespace_escapes_are_rejected_when_used_whole() {
for usage in [
"consume(Devup)",
"Object.keys(Devup)",
"function f() { return Devup }",
"const value = { ns: Devup }",
"const value = { Devup }",
"const value = [Devup]",
"export default Devup",
"export default { Devup }",
"const value = typeof Devup",
"let D = Devup; consume(D)",
"var D = Devup; consume(D)",
"const D = Devup; spyOn(D, 'css')",
"const D = Devup; const E = D; consume(E)",
"const D = Devup; export default D",
"function f() { const D = Devup; return D }",
"const E = D; const D = Devup; consume(E)",
"export { D }; const D = Devup",
"consume((Devup))",
"consume(Devup as object)",
] {
let source = format!("{NS}{usage}\n");

let error = namespace_error(&source);

assert!(error.contains("test.tsx:2:"), "{usage}: {error}");
assert!(
error.contains("cannot use") && error.contains("read its members by name"),
"{usage}: {error}"
);
}
}

#[test]
#[serial]
fn safe_namespace_reads_preserve_semantic_identity() {
let source = format!(
"{NS}const D = Devup; const E = D;\nconst {{ css: c }} = E;\nexport const a = c({{ color: 'red' }});\nexport const b = D.css({{ color: 'blue' }});\nexport type N = typeof Devup;\nexport type A = typeof D;\nexport function f(Devup) {{ return spyOn(Devup, 'css') }}\nexport function g(D) {{ return D }}\nexport {{ Devup as UI }};\n"
);

let code = namespace_code(&source);

assert!(!code.contains("c({") && !code.contains("D.css("), "{code}");
assert!(code.contains("spyOn(Devup, \"css\")"), "{code}");
assert!(code.contains("return D"), "{code}");
assert!(
code.contains("typeof Devup") && code.contains("typeof D"),
"{code}"
);
assert!(code.contains("export * as UI"), "{code}");
}

#[test]
#[serial]
fn type_only_and_unrelated_namespaces_remain_unchanged() {
for source in [
"import type * as Devup from '@devup-ui/react'; export type N = typeof Devup;",
"import * as Devup from 'other'; spyOn(Devup, 'css');",
"import * as Devup from '@devup-ui/react'; function f() { const Devup = {}; return Devup }",
] {
let result = rewrite(source, "test.tsx", "@devup-ui/react", None);

assert!(matches!(result, Barreled::Unchanged));
}
}

#[test]
#[serial]
fn reexported_namespaces_are_rejected_when_their_runtime_value_escapes() {
for (index, (barrel, import)) in [
(
"export { css } from '@devup-ui/react'",
"import * as Devup from './ui'",
),
(
"export * as Devup from '@devup-ui/react'",
"import { Devup } from './ui'",
),
(
"import * as D from '@devup-ui/react'; export default D",
"import Devup from './ui'",
),
(
"import * as D from '@devup-ui/react'; export const Devup = D",
"import { Devup } from './ui'",
),
(
"export * as Devup from './inner'",
"import { Devup } from './ui'",
),
]
.into_iter()
.enumerate()
{
let source = format!("{import}\nspyOn(Devup, 'css')\n");
let project = Project::new(
&format!("escape-{index}"),
&[
("ui.ts", barrel),
("inner.ts", "export { css } from '@devup-ui/react'"),
("app.tsx", &source),
],
);

let error = project.error("app.tsx");

assert!(error.contains("app.tsx:2:7:"), "{error}");
assert!(
error.contains("cannot use") && error.contains("import"),
"{error}"
);
}
}

#[test]
#[serial]
fn default_namespace_import_is_rejected_when_passed_to_runtime() {
let source = "import Devup from '@devup-ui/react'\nspyOn(Devup, 'css')\n";

let error = namespace_error(source);

assert!(
error.contains("test.tsx:2:7: `Devup` cannot use `Devup`"),
"{error}"
);
}
19 changes: 11 additions & 8 deletions libs/extractor/src/barrel/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use crate::{
has_devup_ui_with,
};

#[path = "namespace_escape_tests.rs"]
mod namespace_escape_tests;

/// A project of real files on disk, removed when dropped
struct Project {
root: PathBuf,
Expand Down Expand Up @@ -838,12 +841,13 @@ fn long_code_in_an_error_is_shortened() {

#[test]
#[serial]
fn namespace_passed_whole_is_left_to_the_runtime() {
let code = namespace_code(&format!(
"{NS}export const a = Object.keys(Devup)\nexport const b = ((fn) => fn)(Devup)\nexport default Devup\nexport const c = typeof Devup\n"
));
assert!(code.contains("Object.keys(Devup)"), "{code}");
assert!(code.contains("export default Devup"), "{code}");
fn namespace_passed_whole_is_a_located_build_error() {
let error = namespace_error(&format!("{NS}spyOn(Devup, 'css')\n"));
assert!(
error.contains("test.tsx:2:7: `Devup` cannot use `Devup` at build time"),
"{error}"
);
assert!(error.contains("read its members by name"), "{error}");
}

#[test]
Expand Down Expand Up @@ -983,11 +987,10 @@ fn barrel_module_exports_what_it_aliases_as_re_exports() {
#[serial]
fn namespace_aliases_read_members() {
let code = namespace_code(&format!(
"{NS}const D = Devup\nexport const a = <D.Box bg=\"red\" />\nexport const b = D.css({{ color: 'blue' }})\nlet L = Devup\nexport const c = L\n"
"{NS}const D = Devup\nexport const a = <D.Box bg=\"red\" />\nexport const b = D.css({{ color: 'blue' }})\n"
));
assert!(code.contains("<div className="), "{code}");
assert!(!code.contains("css("), "{code}");
assert!(code.contains("export const c = L"), "{code}");
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions libs/extractor/src/build_time_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ const GLOBALS: [&str; 18] = [

/// The members of `Math` every engine gives exactly; the others are
/// approximations that may differ in their last digits
const EXACT_MATH: [&str; 20] = [
"abs", "ceil", "floor", "round", "trunc", "sign", "max", "min", "sqrt", "fround", "imul",
"clz32", "PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2",
];
pub(crate) mod exact_math;
#[cfg(test)]
mod w22_tests;
use exact_math::EXACT_MATH;

/// Members giving what the locale, the Unicode data of the engine or chance
/// make them, and `toString`, which engines only approximate with a radix:
Expand Down
Loading
Loading