use oxc_allocator::Allocator;
use oxc_ast::ast::{Argument, Expression, ObjectPropertyKind, PropertyKey, Statement};
use oxc_parser::Parser;
use oxc_span::{GetSpan, SourceType};
use vize_carton::FxHashMap;
use vize_carton::{String, ToCompactString};
#[derive(Debug, Clone)]
pub struct PropTypeInfo {
pub js_type: String,
pub ts_type: Option<String>,
pub optional: bool,
pub nullable: bool,
}
fn strip_ts_comments(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
let mut in_string = false;
let mut string_char = b'"';
while i < bytes.len() {
if in_string {
if bytes[i] == string_char && (i == 0 || bytes[i - 1] != b'\\') {
in_string = false;
}
result.push(bytes[i] as char);
i += 1;
continue;
}
match bytes[i] {
b'\'' | b'"' | b'`' => {
in_string = true;
string_char = bytes[i];
result.push(bytes[i] as char);
i += 1;
}
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
if i + 1 < bytes.len() {
i += 2; }
}
_ => {
result.push(bytes[i] as char);
i += 1;
}
}
}
result
}
fn join_union_continuation_lines(input: &str) -> String {
let lines: Vec<&str> = input.lines().collect();
let mut result = String::with_capacity(input.len());
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with('|') || trimmed.starts_with('&') {
result.push(' ');
result.push_str(trimmed);
} else {
if i > 0 {
result.push('\n');
}
result.push_str(line);
}
}
result
}
pub fn extract_prop_types_from_type(type_args: &str) -> Vec<(String, PropTypeInfo)> {
let mut props = Vec::new();
let stripped = strip_ts_comments(type_args);
let joined = join_union_continuation_lines(&stripped);
let content = joined.trim();
let content = if content.starts_with('{') && content.ends_with('}') {
&content[1..content.len() - 1]
} else {
content
};
let mut depth: i32 = 0;
let mut current = String::default();
let chars: Vec<char> = content.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'{' | '<' | '(' | '[' => {
depth += 1;
current.push(c);
}
'}' | ')' | ']' => {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
'>' => {
if i > 0 && chars[i - 1] == '=' {
current.push(c);
} else {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
}
',' | ';' if depth <= 0 => {
extract_prop_type_info(¤t, &mut props);
current.clear();
depth = 0;
}
'\n' if depth <= 0 => {
let trimmed_current = current.trim();
if !trimmed_current.is_empty() && !trimmed_current.ends_with(':') {
extract_prop_type_info(¤t, &mut props);
current.clear();
depth = 0;
}
}
_ => current.push(c),
}
i += 1;
}
extract_prop_type_info(¤t, &mut props);
props
}
fn extract_prop_type_info(segment: &str, props: &mut Vec<(String, PropTypeInfo)>) {
let trimmed = segment.trim();
if trimmed.is_empty() {
return;
}
if let Some(colon_pos) = trimmed.find(':') {
let name_part = &trimmed[..colon_pos];
let type_part = &trimmed[colon_pos + 1..];
let optional = name_part.ends_with('?') || type_includes_top_level_undefined(type_part);
let nullable = type_includes_top_level_null(type_part);
let name = name_part.trim().trim_end_matches('?').trim();
if !name.is_empty() && is_valid_identifier(name) {
let ts_type_str = type_part.trim().to_compact_string();
let js_type = ts_type_to_js_type(&ts_type_str);
if !props.iter().any(|(n, _)| n == name) {
props.push((
name.to_compact_string(),
PropTypeInfo {
js_type,
ts_type: Some(ts_type_str),
optional,
nullable,
},
));
}
}
}
}
fn type_includes_top_level_undefined(ts_type: &str) -> bool {
split_type_at_top_level(ts_type.trim(), '|')
.into_iter()
.any(|part| part.trim() == "undefined")
}
fn type_includes_top_level_null(ts_type: &str) -> bool {
split_type_at_top_level(ts_type.trim(), '|')
.into_iter()
.any(|part| part.trim() == "null")
}
pub fn add_null_to_runtime_type(js_type: &str, nullable: bool) -> String {
if !nullable || js_type == "null" {
return js_type.to_compact_string();
}
if js_type.starts_with('[') && js_type.ends_with(']') {
let inner = &js_type[1..js_type.len() - 1];
if inner
.split(',')
.map(|part| part.trim())
.any(|part| part == "null")
{
return js_type.to_compact_string();
}
let mut result = String::with_capacity(js_type.len() + 6);
result.push('[');
result.push_str(inner);
if !inner.trim().is_empty() {
result.push_str(", ");
}
result.push_str("null");
result.push(']');
return result;
}
let mut result = String::with_capacity(js_type.len() + 8);
result.push('[');
result.push_str(js_type);
result.push_str(", null]");
result
}
fn split_type_at_top_level(s: &str, delimiter: char) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::default();
let mut depth: i32 = 0;
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'(' | '[' | '{' | '<' => {
depth += 1;
current.push(c);
}
')' | ']' | '}' => {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
'>' => {
if i > 0 && chars[i - 1] == '=' {
current.push(c);
} else {
if depth > 0 {
depth -= 1;
}
current.push(c);
}
}
c2 if c2 == delimiter && depth == 0 => {
parts.push(std::mem::take(&mut current));
}
_ => current.push(c),
}
i += 1;
}
if !current.is_empty() || !parts.is_empty() {
parts.push(current);
}
parts
}
fn contains_top_level_arrow(s: &str) -> bool {
let mut depth: i32 = 0;
let chars: Vec<char> = s.chars().collect();
for i in 0..chars.len() {
match chars[i] {
'(' | '[' | '{' | '<' => depth += 1,
')' | ']' | '}' => {
if depth > 0 {
depth -= 1;
}
}
'>' => {
if i > 0 && chars[i - 1] == '=' {
if depth == 0 {
return true;
}
} else if depth > 0 {
depth -= 1;
}
}
_ => {}
}
}
false
}
fn ts_type_to_js_type(ts_type: &str) -> String {
let ts_type = ts_type.trim();
let ts_type = if ts_type.starts_with("readonly ") {
ts_type.strip_prefix("readonly ").unwrap().trim()
} else {
ts_type
};
if (ts_type.starts_with('"') && ts_type.ends_with('"'))
|| (ts_type.starts_with('\'') && ts_type.ends_with('\''))
{
return "String".to_compact_string();
}
if ts_type.parse::<f64>().is_ok() {
return "Number".to_compact_string();
}
if ts_type == "true" || ts_type == "false" {
return "Boolean".to_compact_string();
}
if contains_top_level_arrow(ts_type) {
return "Function".to_compact_string();
}
{
let parts = split_type_at_top_level(ts_type, '|');
if parts.len() > 1 {
let meaningful: Vec<&str> = parts
.iter()
.map(|p| p.trim())
.filter(|p| *p != "undefined" && *p != "null")
.collect();
if meaningful.is_empty() {
return "null".to_compact_string();
}
let mut js_types: Vec<String> = Vec::new();
for part in &meaningful {
let jt = ts_type_to_js_type(part);
if !js_types.contains(&jt) {
js_types.push(jt);
}
}
if js_types.len() == 1 {
return js_types.into_iter().next().unwrap();
}
let joined = js_types.join(", ");
let mut result = String::with_capacity(joined.len() + 2);
result.push('[');
result.push_str(&joined);
result.push(']');
return result;
}
}
match ts_type.to_lowercase().as_str() {
"string" => "String".to_compact_string(),
"number" => "Number".to_compact_string(),
"boolean" => "Boolean".to_compact_string(),
"object" => "Object".to_compact_string(),
"function" => "Function".to_compact_string(),
"symbol" => "Symbol".to_compact_string(),
_ => {
if ts_type.ends_with("[]") || ts_type.starts_with("Array<") {
"Array".to_compact_string()
} else if ts_type.starts_with('{') || contains_top_level_colon(ts_type) {
"Object".to_compact_string()
} else if ts_type.starts_with('(') && ts_type.contains("=>") {
"Function".to_compact_string()
} else {
let type_name = ts_type.split('<').next().unwrap_or(ts_type).trim();
match type_name {
"Date" | "RegExp" | "Error" | "Map" | "Set" | "WeakMap" | "WeakSet"
| "Promise" | "ArrayBuffer" | "DataView" | "Int8Array" | "Uint8Array"
| "Int16Array" | "Uint16Array" | "Int32Array" | "Uint32Array"
| "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array"
| "URL" | "URLSearchParams" | "FormData" | "Blob" | "File" => {
type_name.to_compact_string()
}
"Ref"
| "ShallowRef"
| "ComputedRef"
| "WritableComputedRef"
| "MaybeRef"
| "MaybeRefOrGetter"
| "Readonly"
| "UnwrapRef"
| "Reactive"
| "ShallowReactive"
| "ToRef"
| "ToRefs" => "Object".to_compact_string(),
_ => "null".to_compact_string(),
}
}
}
}
}
fn contains_top_level_colon(s: &str) -> bool {
let mut depth: i32 = 0;
let chars: Vec<char> = s.chars().collect();
for i in 0..chars.len() {
match chars[i] {
'(' | '[' | '{' | '<' => depth += 1,
')' | ']' | '}' => {
if depth > 0 {
depth -= 1;
}
}
'>' => {
if i > 0 && chars[i - 1] == '=' {
} else if depth > 0 {
depth -= 1;
}
}
':' if depth == 0 => return true,
_ => {}
}
}
false
}
pub fn resolve_prop_js_type(
ts_type: &str,
interfaces: &FxHashMap<String, String>,
type_aliases: &FxHashMap<String, String>,
) -> Option<String> {
let trimmed = ts_type.trim();
if trimmed.is_empty() {
return None;
}
let js_type = ts_type_to_js_type(trimmed);
if js_type != "null" {
return None; }
let base_name = if let Some(idx) = trimmed.find('<') {
trimmed[..idx].trim()
} else {
trimmed
};
if let Some(body) = type_aliases.get(base_name) {
let resolved_type = ts_type_to_js_type(body.trim());
if resolved_type != "null" {
return Some(resolved_type);
}
if body.contains('{') {
return Some("Object".to_compact_string());
}
}
if let Some(body) = interfaces.get(base_name) {
let _ = body;
return Some("Object".to_compact_string());
}
None
}
pub fn strip_readonly_prefix(ts_type: &str) -> &str {
let trimmed = ts_type.trim();
if let Some(rest) = trimmed.strip_prefix("readonly ") {
rest.trim()
} else {
trimmed
}
}
pub fn extract_emit_names_from_type(type_args: &str) -> Vec<String> {
let mut emits = Vec::new();
let trimmed = type_args.trim();
let is_shorthand = trimmed.starts_with('{')
&& trimmed.contains('[')
&& !trimmed.contains("(e:")
&& !trimmed.contains("(event:");
if is_shorthand {
let inner = if trimmed.starts_with('{') && trimmed.ends_with('}') {
&trimmed[1..trimmed.len() - 1]
} else {
trimmed
};
for segment in inner.split([';', '\n']) {
let seg = segment.trim();
if seg.is_empty() {
continue;
}
if let Some(colon_pos) = seg.find(':') {
let name = seg[..colon_pos].trim();
let name = name.trim_matches(|c| c == '\'' || c == '"');
if !name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
emits.push(name.to_compact_string());
}
}
}
if !emits.is_empty() {
return emits;
}
}
let mut in_string = false;
let mut quote_char = ' ';
let mut current_string = String::default();
for c in type_args.chars() {
if !in_string && (c == '\'' || c == '"') {
in_string = true;
quote_char = c;
current_string.clear();
} else if in_string && c == quote_char {
in_string = false;
if !current_string.is_empty() {
emits.push(current_string.clone());
}
} else if in_string {
current_string.push(c);
}
}
emits
}
pub fn extract_with_defaults_defaults(with_defaults_args: &str) -> FxHashMap<String, String> {
let mut defaults = FxHashMap::default();
let trimmed = with_defaults_args.trim();
if trimmed.is_empty() {
return defaults;
}
const WRAP_PREFIX: &str = "const __vize_defaults__ = ";
let mut wrapped = String::with_capacity(WRAP_PREFIX.len() + trimmed.len() + 1);
wrapped.push_str(WRAP_PREFIX);
wrapped.push_str(trimmed);
wrapped.push(';');
let allocator = Allocator::default();
let parse_result = Parser::new(
&allocator,
&wrapped,
SourceType::default().with_typescript(true),
)
.parse();
if !parse_result.errors.is_empty() {
return defaults;
}
let Some(Statement::VariableDeclaration(var_decl)) = parse_result.program.body.first() else {
return defaults;
};
let Some(declarator) = var_decl.declarations.first() else {
return defaults;
};
let Some(Expression::CallExpression(call)) = declarator.init.as_ref() else {
return defaults;
};
let Expression::Identifier(callee) = &call.callee else {
return defaults;
};
if callee.name.as_str() != "withDefaults" {
return defaults;
}
let Some(Argument::ObjectExpression(obj)) = call.arguments.get(1) else {
return defaults;
};
for property in obj.properties.iter() {
let ObjectPropertyKind::ObjectProperty(prop) = property else {
continue;
};
let key = match &prop.key {
PropertyKey::StaticIdentifier(id) => id.name.to_compact_string(),
PropertyKey::StringLiteral(lit) => lit.value.to_compact_string(),
PropertyKey::NumericLiteral(lit) => lit.value.to_compact_string(),
_ => continue,
};
let Some(value_start) = (prop.value.span().start as usize).checked_sub(WRAP_PREFIX.len())
else {
continue;
};
let Some(value_end) = (prop.value.span().end as usize).checked_sub(WRAP_PREFIX.len())
else {
continue;
};
if let Some(value_src) = trimmed.get(value_start..value_end) {
defaults.insert(key, value_src.to_compact_string());
}
}
defaults
}
pub fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' || c == '$' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}