-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Goal
Create crates/rafters-adapters/src/ast_extract.rs -- a shared module that uses oxc_parser (already a workspace dep at v0.72) to extract component metadata from JS/TS source code. This replaces all regex-based extraction and is framework-agnostic: it handles script-level declarations (variantClasses, sizeClasses, baseClasses, props interfaces) that are identical across React, Solid, Preact, and the script sections of Vue/Svelte SFCs. JSX-specific analysis (element tag discovery, children detection) is also included here for use by JSX-based adapters.
Exact Implementation Requirements
Required Struct/Function Signatures
// crates/rafters-adapters/src/ast_extract.rs
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_parser::Parser;
use oxc_span::SourceType;
use crate::traits::TransformError;
/// What we learn from module-level JS/TS declarations (any framework).
#[derive(Debug, Clone, Default)]
pub struct ModuleExtracts {
pub variant_lookup: Vec<(String, String)>,
pub size_lookup: Vec<(String, String)>,
pub base_classes: String,
pub disabled_classes: String,
pub component_name: Option<String>,
pub interface_props: Vec<String>,
}
/// What we learn from analyzing JSX output (React/Solid/Preact).
#[derive(Debug, Clone, Default)]
pub struct JsxAnalysis {
/// The HTML element tag rendered by the component (e.g., "button", "input", "span")
pub element_tag: Option<String>,
/// Whether the component renders {children}
pub has_children: bool,
/// Whether the root element is self-closing (e.g., <input />)
pub self_closing: bool,
}
/// Local class variables found inside a component function body.
#[derive(Debug, Clone, Default)]
pub struct LocalClassVars {
pub base_classes: Option<String>,
pub disabled_classes: Option<String>,
}
/// Parse JS/TS source with oxc_parser, walk module-level declarations.
/// Extracts variantClasses, sizeClasses, component name, interface props.
pub fn extract_module_level(source: &str) -> Result<ModuleExtracts, TransformError>
/// Extract key-value entries from an ObjectExpression AST node.
/// Handles: `{ key: 'value', key2: "value2" }`
pub fn extract_object_entries(expr: &Expression) -> Option<Vec<(String, String)>>
/// Extract string value from an Expression.
/// Handles: StringLiteral, BinaryExpression (concatenation), TemplateLiteral (no expressions).
pub fn extract_string_value(expr: &Expression) -> Option<String>
/// Extract prop names from destructured function parameters.
/// Skips: children, className, style, ref, key, ...rest patterns.
pub fn extract_props_from_params(params: &FormalParameters) -> Vec<String>
/// Extract prop names from a TypeScript interface declaration.
/// Skips: children, className, style, ref, key.
pub fn extract_props_from_interface(decl: &TSInterfaceDeclaration) -> Vec<String>
/// Extract local class variable declarations from within a function body.
/// Finds patterns: `const base = '...'`, `const baseClasses = '...' + '...'`,
/// `const disabledCls = '...'`, `const disabledClasses = '...'`
pub fn extract_local_class_vars(body: &FunctionBody) -> LocalClassVars
/// Analyze a JSX function body to discover what the component renders.
/// Walks return statements, finds root JSX element.
pub fn analyze_jsx_body(body: &FunctionBody) -> JsxAnalysis
/// Find the first HTML element tag from JSX return statements.
/// Only returns lowercase tags (HTML elements). Uppercase = component references, skipped.
/// Handles: ReturnStatement, ParenthesizedExpression, ConditionalExpression (both branches).
pub fn find_jsx_element_tag(body: &FunctionBody) -> Option<String>
/// Check if JSX children are rendered.
/// Walks JSX children for JSXExpressionContainer containing identifier `children`.
pub fn jsx_has_children(body: &FunctionBody) -> bool
// Internal helper -- not public
/// Unwrap TS wrappers: TSAsExpression, TSSatisfiesExpression, ParenthesizedExpression.
fn unwrap_ts_wrappers(expr: &Expression) -> &ExpressionBehavior Requirements
- Parse with
oxc_parser::ParserusingSourceType::tsx()-- handles both .ts and .tsx extract_module_levelmust walk all top-levelVariableDeclarationnodes and findvariantClasses/sizeClassesby variable name, then callextract_object_entrieson the init expressionextract_module_levelmust walkExportDefaultDeclarationandExportNamedDeclarationto find the component name (function name orconst Name = React.forwardRef(...))extract_module_levelmust findTSInterfaceDeclarationnodes matching*Propspattern and extract prop namesextract_string_valuemust handle string concatenation ('a' + 'b') by recursively walkingBinaryExpressionwithBinaryOperator::Additionextract_string_valuemust handleTemplateLiteralwith zero expressions (just quasis)find_jsx_element_tagmust only return lowercase tag names (HTML elements). Uppercase names are React component references and must be skippedfind_jsx_element_tagmust handleConditionalExpression(ternary) by analyzing both consequent and alternate branchesjsx_has_childrenmust detect{children}in JSX expression containers at any nesting depthunwrap_ts_wrappersmust recursively unwrapTSAsExpression,TSSatisfiesExpression, andParenthesizedExpressionto get the underlying expression- All functions must work with real Rafters component patterns (see test cases below)
Error Handling
extract_module_levelreturnsTransformError::ParseErrorifoxc_parserfailsextract_object_entriesandextract_string_valuereturnNonefor unrecognized patterns (not errors)- Never panic on unexpected AST shapes -- return defaults
Acceptance Criteria
Functional Tests Required
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_variant_classes_from_record() {
let source = r#"
const variantClasses: Record<string, string> = {
default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
};
export function Button() { return <button />; }
"#;
let result = extract_module_level(source).unwrap();
assert_eq!(result.variant_lookup.len(), 3);
assert_eq!(result.variant_lookup[0].0, "default");
assert!(result.variant_lookup[0].1.contains("bg-primary"));
}
#[test]
fn extracts_size_classes() {
let source = r#"
const variantClasses = { default: '' };
const sizeClasses: Record<string, string> = {
sm: 'h-8 rounded-md px-3 text-xs',
default: 'h-9 px-4 py-2',
lg: 'h-10 rounded-md px-8',
};
export function Button() { return <button />; }
"#;
let result = extract_module_level(source).unwrap();
assert_eq!(result.size_lookup.len(), 3);
assert!(result.size_lookup[0].1.contains("h-8"));
}
#[test]
fn extracts_props_from_interface() {
let source = r#"
interface ButtonProps {
variant?: string;
size?: string;
disabled?: boolean;
children?: React.ReactNode;
className?: string;
}
const variantClasses = { default: '' };
export function Button({ variant, size, disabled, children }: ButtonProps) {
return <button />;
}
"#;
let result = extract_module_level(source).unwrap();
assert!(result.interface_props.contains(&"variant".to_string()));
assert!(result.interface_props.contains(&"size".to_string()));
assert!(result.interface_props.contains(&"disabled".to_string()));
// children, className should be filtered out
assert!(!result.interface_props.contains(&"children".to_string()));
assert!(!result.interface_props.contains(&"className".to_string()));
}
#[test]
fn handles_ts_as_expression() {
let source = r#"
const variantClasses = {
default: 'bg-primary',
} as Record<string, string>;
export function Button() { return <button />; }
"#;
let result = extract_module_level(source).unwrap();
assert!(!result.variant_lookup.is_empty());
}
#[test]
fn handles_satisfies_expression() {
let source = r#"
const variantClasses = {
default: 'bg-primary',
} satisfies Record<string, string>;
export function Button() { return <button />; }
"#;
let result = extract_module_level(source).unwrap();
assert!(!result.variant_lookup.is_empty());
}
}Rust Requirements
- Must compile with
cargo clippy --all-targets -- -D warnings - No
unwrap()on user-provided data -- only on internal invariants - No
unsafecode - All public items documented with
///doc comments - Tests use real component source patterns from
packages/ui/src/components/ui/
What NOT to Include
- Framework-specific adapters (separate issue)
- Web Component generation (separate issue)
- Knobs/controls (separate issue)
- Any hardcoded element behaviors or static lookup tables
File Locations
- Implementation:
crates/rafters-adapters/src/ast_extract.rs - Module declaration:
crates/rafters-adapters/src/lib.rs(addpub mod ast_extract;) - Tests: inline
#[cfg(test)] mod testsinast_extract.rs
Integration Requirements
Dependencies
oxc_parser,oxc_allocator,oxc_ast,oxc_spanv0.72 (already in workspace Cargo.toml)crate::traits::TransformErrorfor error types- No new external dependencies
Usage Example
use rafters_adapters::ast_extract::{extract_module_level, analyze_jsx_body};
let source = std::fs::read_to_string("button.tsx").unwrap();
let extracts = extract_module_level(&source)?;
// extracts.variant_lookup, extracts.size_lookup, etc.Success Criteria
- All functional tests pass
-
cargo clippy -p rafters-adapters --all-targetsclean -
cargo test -p rafters-adapterspasses - No regex crate usage in this module
- Correctly parses real Button, Input, Badge component patterns
This issue is complete when: extract_module_level() and analyze_jsx_body() correctly extract all metadata from real Rafters component source files (Button, Input, Badge patterns) using the oxc AST, with zero regex.
Context & References
- Replaces: refactor(docs-rs): Replace regex JSX parsing with oxc_parser AST #673
- Related: refactor(docs-rs): Load component metadata from Rafters design system instead of hardcoding #674
- oxc_parser docs: https://docs.rs/oxc_parser
- Real component patterns:
packages/ui/src/components/ui/button.tsx,input.tsx,badge.tsx