Skip to content
Open
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
5e167b6
fix(extractor): report original parse and module reference locations
owjs3901 Oct 4, 2026
5574d55
fix(extractor): locate deterministic stylesheet evaluation failures
owjs3901 Oct 4, 2026
eceb1ce
fix(wasm): preserve located module resolver failure causes
owjs3901 Oct 4, 2026
dca611f
fix(extractor): prove module effects before exact stylesheet evaluation
owjs3901 Oct 4, 2026
06c66b9
fix(extractor): reject recoverable source and semantic parse errors
owjs3901 Oct 4, 2026
400cf89
fix(extractor): reject synthetic reflection during static evaluation
owjs3901 Oct 4, 2026
1949a6b
test(extractor): cover evaluation setup and naming seams
owjs3901 Oct 4, 2026
920d8ad
chore(wasm): record located diagnostics patch changepack
owjs3901 Oct 4, 2026
cc0aaf6
test(extractor): cover stylesheet precompute errors and import proof …
owjs3901 Oct 4, 2026
3dc297c
fix(extractor): match import packages at path boundaries
owjs3901 Oct 4, 2026
042d6a6
fix(extractor): preserve evaluated helper effects and opaque CSS reads
owjs3901 Oct 5, 2026
27666e7
chore(wasm): document helper CSS diagnostics in patch changepack
owjs3901 Oct 5, 2026
6d0c9c8
test(extractor): cover portable CSS paths and nested member provenance
owjs3901 Oct 5, 2026
78ef660
merge: update PR #757 with PR #750
owjs3901 Oct 8, 2026
29c0467
merge: update PR #757 with PR #750
owjs3901 Oct 17, 2026
0f3ccae
fix(extractor): intern repeated evaluated css key slots
owjs3901 Oct 17, 2026
304779c
test(extractor): cover evaluated stylesheet bridge contracts
owjs3901 Oct 17, 2026
c33e18e
merge: update PR #757 with PR #750
sisyphus-dev-ai Oct 22, 2026
425eb47
test(extractor): batch deep heap fixture construction
sisyphus-dev-ai Oct 22, 2026
f497aa8
test(extractor): trim deep fixture loop bookkeeping
sisyphus-dev-ai Oct 22, 2026
6d68063
test(extractor): cover captured stylesheet contracts
sisyphus-dev-ai Oct 22, 2026
1d6f791
test(extractor): cover bound namespace tag inlining
sisyphus-dev-ai Oct 22, 2026
7b4e844
merge: update PR #757 with settled main
sisyphus-dev-ai Oct 23, 2026
241d7cf
ci: allow 180 seconds per coverage test binary
sisyphus-dev-ai Oct 24, 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
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_css_capture_validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Narrow CSS capture validation states and cover namespace tags, registered template composition, nested responsive rejection and bounded-call checker errors without changing runtime behavior.",
"date": "2026-10-22T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_deep_fixture_cost.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 100000-level heap stress coverage while reducing fixture construction overhead. No new build errors or runtime behavior changes.",
"date": "2026-10-22T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_located_diagnostics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Report extraction, module-resolution and stylesheet execution failures at their original source locations with the cause and repair. Reject recoverable syntax errors, nondeterministic build-time reads, synthetic descriptor/function-source and legacy accessor reflection, and unsafe stylesheet execution fallbacks instead of silently changing CSS. Preserve module resolver and serialization exceptions, evaluate transitive helper side effects, and prevent placeholder-name collisions from losing styles. Match import package names only at exact or slash-delimited boundaries so sibling data packages remain external. Retain helper CSS loads in evaluation order with deduplicated portable relative specifiers and query suffixes, without CSS content dependencies. Report missing CSS or incompatible path roots as located errors, and reject host-recorded unknown CSS export observations even when caught; unused bindings, object passing and typeof follow default TypeScript/JavaScript import semantics.",
"date": "2026-10-01T00:00:00.000Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ fn build_reports_failing_file_when_fixture_contains_malformed_jsx() {
};
// Then: the typed error retains its failing file and parser context.
assert_eq!(error.file, "src/shared/s00.tsx");
assert_eq!(error.message, "src/shared/s00.tsx:1:61: Unexpected token");
assert_eq!(
error.message,
"src/shared/s00.tsx:1:61: Cannot parse source: Unexpected token. Fix: correct the syntax at this location"
);
assert_eq!(
error.to_string(),
"src/shared/s00.tsx: src/shared/s00.tsx:1:61: Unexpected token"
"src/shared/s00.tsx: src/shared/s00.tsx:1:61: Cannot parse source: Unexpected token. Fix: correct the syntax at this location"
);
assert_eq!(css::atom_hoist::atom_hoist_threshold(), None);
assert_eq!(css::file_routes::get_file_routes().len(), 0);
Expand Down
104 changes: 74 additions & 30 deletions bindings/devup-ui-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use std::collections::{BTreeMap, HashMap};
use std::fmt::Display;
use std::sync::{LazyLock, Mutex};
use wasm_bindgen::prelude::*;
mod resolver_state;
use resolver_state::ResolverState;

mod state;
#[cfg(test)]
Expand Down Expand Up @@ -416,6 +418,7 @@ pub fn code_extract_internal(
import_aliases,
SourceMapMode::Generate,
None,
None,
)
}

Expand All @@ -441,6 +444,7 @@ pub fn code_extract_without_source_map_internal(
import_aliases,
SourceMapMode::Skip,
None,
None,
)
}

Expand Down Expand Up @@ -469,6 +473,7 @@ pub fn code_extract_with_modules_internal(
import_aliases,
SourceMapMode::Generate,
Some(resolver),
None,
)
}

Expand All @@ -484,6 +489,7 @@ fn code_extract_internal_impl(
import_aliases: HashMap<String, ImportAlias>,
source_map: SourceMapMode,
resolver: Option<&ModuleResolver>,
resolver_state: Option<&RefCell<ResolverState>>,
) -> Result<Output, String> {
let option = ExtractOption {
package: package.to_string(),
Expand All @@ -504,6 +510,9 @@ fn code_extract_internal_impl(
(None, SourceMapMode::Skip) => extract_without_source_map(filename, code, option),
};

if let Some(state) = resolver_state {
state.borrow().check()?;
}
match extracted {
Ok(output) => Ok(Output::new(
output.code,
Expand Down Expand Up @@ -532,29 +541,69 @@ pub fn set_module_resolver(resolver: Option<js_sys::Function>) {
#[cfg(not(tarpaulin_include))]
fn call_module_resolver(
resolver: &js_sys::Function,
specifier: &str,
importer: &str,
request: resolver_state::ResolverRequest,
state: &RefCell<ResolverState>,
) -> Option<ResolvedModule> {
let module = resolver
.call2(&JsValue::NULL, &specifier.into(), &importer.into())
.ok()?;
let field = |name: &str| {
js_sys::Reflect::get(&module, &name.into())
.ok()?
.as_string()
let specifier = request.specifier();
let importer = request.importer();
let message = |error: JsValue| {
if let Some(message) = error.as_string() {
return message;
}
match js_sys::Reflect::get(&error, &"message".into()) {
Ok(value) => value.as_string().unwrap_or_else(|| format!("{error:?}")),
Err(_) => format!("{error:?}"),
}
};
Some(ResolvedModule {
path: field("path")?,
code: field("code")?,
})
let resolved = (|| -> Result<Option<ResolvedModule>, String> {
let module = resolver
.call2(&JsValue::NULL, &specifier.into(), &importer.into())
.map_err(message)?;
if module.is_null() || module.is_undefined() {
return Ok(None);
}
if !module.is_object() || js_sys::Array::is_array(&module) {
return Err("malformed module resolver result: expected an object {path:string,code:string} or null/undefined".to_string());
}
let field = |name: &str| -> Result<String, String> {
js_sys::Reflect::get(&module, &name.into())
.map_err(message)?
.as_string()
.ok_or_else(|| {
format!("malformed module resolver result: `{name}` must be a string")
})
};
let path = field("path")?;
if path.is_empty() {
return Err(
"malformed module resolver result: `path` must be a non-empty string".to_string(),
);
}
Ok(Some(ResolvedModule {
path,
code: field("code")?,
}))
})();
match resolved {
Ok(Some(module)) => {
state.borrow_mut().cache(&module, &request);
Some(module)
}
Ok(None) => None,
Err(cause) => {
state.borrow_mut().record(&request, &cause);
None
}
}
}

/// The resolver set by `setModuleResolver`, if any
#[cfg(not(tarpaulin_include))]
fn resolver_from_js() -> Option<Box<ModuleResolver>> {
fn resolver_from_js(state: std::rc::Rc<RefCell<ResolverState>>) -> Option<Box<ModuleResolver>> {
let resolver = MODULE_RESOLVER.with_borrow(Clone::clone)?;
Some(Box::new(move |specifier: &str, importer: &str| {
call_module_resolver(&resolver, specifier, importer)
let request = state.borrow().request(specifier, importer)?;
call_module_resolver(&resolver, request, &state)
}))
}

Expand All @@ -573,9 +622,8 @@ fn code_extract_js(
source_map: SourceMapMode,
) -> Result<Output, JsValue> {
let import_aliases = import_aliases_from_js(import_aliases)?;
let resolver = MODULE_RESOLVER.with_borrow(Clone::clone).map(|resolver| {
move |specifier: &str, importer: &str| call_module_resolver(&resolver, specifier, importer)
});
let state = std::rc::Rc::new(RefCell::new(ResolverState::new(filename, code)));
let resolver = resolver_from_js(std::rc::Rc::clone(&state));
code_extract_internal_impl(
filename,
code,
Expand All @@ -586,9 +634,8 @@ fn code_extract_js(
import_main_css_in_css,
import_aliases,
source_map,
resolver
.as_ref()
.map(|resolver| resolver as &ModuleResolver),
resolver.as_deref(),
Some(&state),
)
.map_err(js_error)
}
Expand Down Expand Up @@ -737,21 +784,18 @@ pub fn get_theme_interface(

#[wasm_bindgen(js_name = "hasDevupUI")]
#[cfg(not(tarpaulin_include))]
#[must_use]
pub fn has_devup_ui_wasm(
filename: &str,
code: &str,
package: &str,
import_aliases: JsValue,
) -> bool {
) -> Result<bool, JsValue> {
let aliases = import_aliases_from_js(import_aliases).unwrap_or_default();
has_devup_ui_with(
filename,
code,
package,
&aliases,
resolver_from_js().as_deref(),
)
let state = std::rc::Rc::new(RefCell::new(ResolverState::new(filename, code)));
let resolver = resolver_from_js(std::rc::Rc::clone(&state));
let result = has_devup_ui_with(filename, code, package, &aliases, resolver.as_deref());
state.borrow().check().map_err(js_error)?;
Ok(result)
}

#[cfg(test)]
Expand Down
16 changes: 14 additions & 2 deletions bindings/devup-ui-wasm/src/reservation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ 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};
use crate::{
MODULE_RESOLVER, call_module_resolver, import_aliases_from_js, js_error,
resolver_state::ResolverState,
};
#[cfg(not(tarpaulin_include))]
use std::{cell::RefCell, rc::Rc};

/// Prepared physical input, keyed exactly as its later real transform.
pub struct ReservationInput {
Expand Down Expand Up @@ -105,7 +110,14 @@ pub fn reserve_class_slots(
..ExtractOption::default()
};
let resolver = MODULE_RESOLVER.with_borrow(Clone::clone).map(|resolver| {
move |specifier: &str, importer: &str| call_module_resolver(&resolver, specifier, importer)
let state = Rc::new(RefCell::new(ResolverState::default()));
for input in &inputs {
state.borrow_mut().add_source(&input.filename, &input.code);
}
move |specifier: &str, importer: &str| {
let request = state.borrow().request(specifier, importer)?;
call_module_resolver(&resolver, request, &state)
}
});
reserve_class_slots_internal(
&inputs,
Expand Down
141 changes: 141 additions & 0 deletions bindings/devup-ui-wasm/src/resolver_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::collections::{BTreeSet, HashMap};

use extractor::ResolvedModule;
use extractor::module_reference::{ModuleReferenceLocation, original_module_reference_locations};

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct OriginSite {
filename: String,
location: ModuleReferenceLocation,
}

#[derive(Clone)]
struct OriginSites {
first: OriginSite,
rest: Vec<OriginSite>,
}

struct ModuleSource {
code: String,
ingress: Option<OriginSites>,
}

/// A callback request with at least one original reference site.
pub(crate) struct ResolverRequest {
specifier: String,
importer: String,
origins: OriginSites,
}

impl ResolverRequest {
pub(crate) fn specifier(&self) -> &str {
&self.specifier
}

pub(crate) fn importer(&self) -> &str {
&self.importer
}
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ResolverFailure {
site: OriginSite,
specifier: String,
importer: String,
cause: String,
}

/// Resolver diagnostics and original module sources owned by one extraction.
#[derive(Default)]
pub(crate) struct ResolverState {
sources: HashMap<String, ModuleSource>,
failures: BTreeSet<ResolverFailure>,
}

impl ResolverState {
pub(crate) fn new(filename: &str, source: &str) -> Self {
Self {
sources: HashMap::from([(
filename.to_string(),
ModuleSource {
code: source.to_string(),
ingress: None,
},
)]),
failures: BTreeSet::new(),
}
}

pub(crate) fn cache(&mut self, module: &ResolvedModule, request: &ResolverRequest) {
self.sources.insert(
module.path.clone(),
ModuleSource {
code: module.code.clone(),
ingress: Some(request.origins.clone()),
},
);
}

pub(crate) fn add_source(&mut self, filename: &str, code: &str) {
self.sources.insert(
filename.to_string(),
ModuleSource {
code: code.to_string(),
ingress: None,
},
);
}

/// Bind original references before calling JS; recursive exploratory reads
/// without a semantic reference retain the real import that loaded the module.
pub(crate) fn request(&self, specifier: &str, importer: &str) -> Option<ResolverRequest> {
let source = self.sources.get(importer)?;
let mut sites = original_module_reference_locations(importer, &source.code, specifier)
.into_iter()
.map(|location| OriginSite {
filename: importer.to_string(),
location,
});
let origins = match sites.next() {
Some(first) => OriginSites {
first,
rest: sites.collect(),
},
None => source.ingress.clone()?,
};
Some(ResolverRequest {
specifier: specifier.to_string(),
importer: importer.to_string(),
origins,
})
}

pub(crate) fn record(&mut self, request: &ResolverRequest, cause: &str) {
for site in std::iter::once(&request.origins.first).chain(&request.origins.rest) {
self.failures.insert(ResolverFailure {
site: site.clone(),
specifier: request.specifier.clone(),
importer: request.importer.clone(),
cause: cause.to_string(),
});
}
}

pub(crate) fn check(&self) -> Result<(), String> {
if self.failures.is_empty() {
Ok(())
} else {
Err(self.failures.iter().map(|failure| {
let OriginSite { filename, location } = &failure.site;
format!(
"{filename}:{}:{}: module resolver failed for `{}` from `{}`: {}\nFix: repair the module resolver; return {{path:string,code:string}} with a non-empty path, or null/undefined for an unresolved module.",
location.line, location.column, failure.specifier, failure.importer, failure.cause,
)
}).collect::<Vec<_>>().join("\n"))
}
}
}

#[cfg(test)]
#[path = "resolver_state_tests.rs"]
mod tests;
Loading
Loading