-
Notifications
You must be signed in to change notification settings - Fork 0
Closed as not planned
Closed as not planned
Copy link
Labels
enhancementNew feature or requestNew feature or request
Description
Goal
Rewrite crates/rafters-adapters/src/react.rs to use the ast_extract module instead of regex patterns. Add element_tag, has_children, and self_closing fields to ComponentStructure. Delete all LazyLock<Regex> statics and regex helper functions.
Exact Implementation Requirements
Required Struct Changes
// crates/rafters-adapters/src/react.rs
/// Extracted component structure from source code.
#[derive(Debug, Clone, Default)]
pub struct ComponentStructure {
pub name: String,
pub variant_lookup: Vec<(String, String)>,
pub size_lookup: Vec<(String, String)>,
pub base_classes: String,
pub disabled_classes: String,
pub default_variant: String,
pub default_size: String,
pub observed_attributes: Vec<String>,
// NEW fields:
/// HTML element tag rendered by the component (e.g., "button", "input", "span").
/// Discovered from JSX return statement. Defaults to "div" if not found.
pub element_tag: String,
/// Whether the component renders children (has {children} in JSX).
pub has_children: bool,
/// Whether the root element is self-closing (e.g., <input />).
pub self_closing: bool,
}Behavior Requirements
ReactAdapter::extract_structure()must:- Call
ast_extract::extract_module_level(source)for variant/size lookups, component name, interface props - Parse source with oxc to find the component function body:
- Walk exported declarations for
React.forwardRef()calls -- unwrap to get inner arrow/function - Or find exported plain function declarations
- Walk exported declarations for
- Call
ast_extract::analyze_jsx_body()on the function body - Call
ast_extract::extract_props_from_params()on function parameters - Call
ast_extract::extract_local_class_vars()on function body for base/disabled classes - Merge local class vars with module-level extracts (local takes precedence)
- Merge interface props with destructured params (union, deduplicated)
- Call
element_tagdefaults to"div"when JSX analysis finds no HTML elementhas_childrendefaults totruewhen not determinableself_closingdefaults tofalse- Remove the
regexcrate fromrafters-adapters/Cargo.tomlif no other module uses it
What to Delete
- All
LazyLock<Regex>statics:COMPONENT_NAME_RE,RECORD_RE,ENTRY_RE,BASE_CLASSES_CONCAT_RE,BASE_CLASSES_SIMPLE_RE,DISABLED_CLASSES_RE,PROPS_INTERFACE_RE,DESTRUCTURE_RE - Functions:
extract_component_name(),extract_record(),extract_base_classes(),parse_concatenated_string(),extract_disabled_classes(),extract_attributes() - The hardcoded common attributes list:
let common = ["variant", "size", "disabled", "loading"];
Error Handling
- Return
TransformError::ParseErrorif oxc parsing fails - Return
TransformError::MissingVariantsif novariantClassesfound (existing behavior preserved) - Gracefully handle components without size classes, base classes, or disabled classes (existing behavior)
Acceptance Criteria
Functional Tests Required
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::TransformContext;
#[test]
fn extracts_variant_classes() {
// Same test as current -- must still pass
let source = r#"
const variantClasses: Record<string, string> = {
default: 'bg-primary text-primary-foreground',
secondary: 'bg-secondary text-secondary-foreground',
};
export function Button() { return <button />; }
"#;
let adapter = ReactAdapter::new();
let result = adapter.transform(source, "button-preview", &TransformContext::default()).unwrap();
assert!(result.web_component.contains("bg-primary"));
}
#[test]
fn discovers_element_tag() {
let source = r#"
const variantClasses = { default: 'bg-primary' };
export function Button({ children }: Props) {
return <button className="btn">{children}</button>;
}
"#;
let adapter = ReactAdapter::new();
let structure = adapter.extract_structure(source).unwrap();
assert_eq!(structure.element_tag, "button");
assert!(structure.has_children);
assert!(!structure.self_closing);
}
#[test]
fn discovers_self_closing_input() {
let source = r#"
const variantClasses = { default: 'border' };
export const Input = React.forwardRef<HTMLInputElement, Props>(
({ className, ...props }, ref) => {
return <input ref={ref} className={className} {...props} />;
}
);
"#;
let adapter = ReactAdapter::new();
let structure = adapter.extract_structure(source).unwrap();
assert_eq!(structure.element_tag, "input");
assert!(!structure.has_children);
assert!(structure.self_closing);
}
#[test]
fn discovers_span_with_children() {
let source = r#"
const variantClasses = { default: 'bg-primary' };
export function Badge({ children }: Props) {
return <span className="badge">{children}</span>;
}
"#;
let adapter = ReactAdapter::new();
let structure = adapter.extract_structure(source).unwrap();
assert_eq!(structure.element_tag, "span");
assert!(structure.has_children);
}
#[test]
fn defaults_element_tag_to_div() {
let source = r#"
const variantClasses = { default: '' };
export function Unknown() { return null; }
"#;
let adapter = ReactAdapter::new();
let structure = adapter.extract_structure(source).unwrap();
assert_eq!(structure.element_tag, "div");
}
#[test]
fn errors_on_missing_variants() {
// Existing behavior preserved
let source = "export function Button() { return <button />; }";
let adapter = ReactAdapter::new();
let result = adapter.transform(source, "button-preview", &TransformContext::default());
assert!(matches!(result, Err(TransformError::MissingVariants)));
}
#[test]
fn extracts_props_without_hardcoded_list() {
let source = r#"
const variantClasses = { default: '' };
interface CardProps {
variant?: string;
elevation?: number;
rounded?: boolean;
children?: React.ReactNode;
}
export function Card({ variant, elevation, rounded, children }: CardProps) {
return <div>{children}</div>;
}
"#;
let adapter = ReactAdapter::new();
let structure = adapter.extract_structure(source).unwrap();
assert!(structure.observed_attributes.contains(&"variant".to_string()));
assert!(structure.observed_attributes.contains(&"elevation".to_string()));
assert!(structure.observed_attributes.contains(&"rounded".to_string()));
// children should NOT be in observed_attributes
assert!(!structure.observed_attributes.contains(&"children".to_string()));
}
}Rust Requirements
- Must compile with
cargo clippy --all-targets -- -D warnings - No
regexcrate usage remaining inreact.rs - All existing tests updated to pass with new implementation
- New tests for element discovery
What NOT to Include
- Changes to
generator.rs(separate issue) - Knobs/controls (separate issue)
inline.rschanges (it has its own regex for a different purpose)
File Locations
- Implementation:
crates/rafters-adapters/src/react.rs - Tests: inline
#[cfg(test)] mod testsinreact.rs - Dependency cleanup:
crates/rafters-adapters/Cargo.toml(removeregexif unused elsewhere)
Integration Requirements
Dependencies
- feat(docs-rs): Create ast_extract module for shared AST extraction with oxc_parser #765 (
ast_extractmodule) oxc_parser,oxc_allocator,oxc_ast,oxc_spanv0.72
Success Criteria
- All existing tests pass (updated for new API)
- New element discovery tests pass
- Zero
LazyLock<Regex>inreact.rs - Zero hardcoded attribute lists
-
cargo clippy -p rafters-adapters --all-targetsclean -
cargo test -p rafters-adapterspasses
This issue is complete when: ReactAdapter::extract_structure() discovers element tag, children support, self-closing status, and all props from the AST with zero regex and zero hardcoded assumptions.
Context & References
- Depends on: feat(docs-rs): Create ast_extract module for shared AST extraction with oxc_parser #765 (ast_extract module)
- Replaces: refactor(docs-rs): Replace regex JSX parsing with oxc_parser AST #673, refactor(docs-rs): Load component metadata from Rafters design system instead of hardcoding #674
- Current regex patterns to delete:
react.rs:148-187 - Current hardcoded attributes:
react.rs:263
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request