use vize_croquis::{BindingType, Croquis, ScopeData, ScopeKind};
use super::{
helpers::{
IMPORT_META_AUGMENTATION, SETUP_SCOPE_HELPER_NAMES, VUE_SETUP_HELPERS, VUE_TYPE_HELPERS,
generate_template_context, to_safe_identifier,
},
props::{
add_generic_defaults, collect_template_prop_names, extract_generic_names,
generate_props_type, generate_props_variables,
},
scope::generate_scope_closures,
types::{VirtualTsGenerationOptions, VirtualTsOptions, VirtualTsOutput, VizeMapping},
};
use vize_carton::append;
use vize_carton::cstr;
use vize_carton::profile;
use vize_carton::{FxHashMap, FxHashSet, String};
pub fn generate_virtual_ts(
summary: &Croquis,
script_content: Option<&str>,
template_ast: Option<&vize_relief::ast::RootNode<'_>>,
template_offset: u32,
) -> VirtualTsOutput {
generate_virtual_ts_with_offsets(
summary,
script_content,
template_ast,
0,
template_offset,
&VirtualTsOptions::default(),
)
}
pub fn generate_virtual_ts_with_offsets(
summary: &Croquis,
script_content: Option<&str>,
template_ast: Option<&vize_relief::ast::RootNode<'_>>,
script_offset: u32,
template_offset: u32,
options: &VirtualTsOptions,
) -> VirtualTsOutput {
generate_virtual_ts_with_offsets_and_checks(
summary,
script_content,
template_ast,
script_offset,
template_offset,
options,
VirtualTsGenerationOptions::default(),
)
}
pub fn generate_virtual_ts_with_offsets_options_api(
summary: &Croquis,
script_content: Option<&str>,
template_ast: Option<&vize_relief::ast::RootNode<'_>>,
script_offset: u32,
template_offset: u32,
options: &VirtualTsOptions,
) -> VirtualTsOutput {
generate_virtual_ts_with_offsets_and_checks(
summary,
script_content,
template_ast,
script_offset,
template_offset,
options,
VirtualTsGenerationOptions {
options_api: true,
..Default::default()
},
)
}
pub fn generate_virtual_ts_with_offsets_legacy_vue2(
summary: &Croquis,
script_content: Option<&str>,
template_ast: Option<&vize_relief::ast::RootNode<'_>>,
script_offset: u32,
template_offset: u32,
options: &VirtualTsOptions,
) -> VirtualTsOutput {
generate_virtual_ts_with_offsets_and_checks(
summary,
script_content,
template_ast,
script_offset,
template_offset,
options,
VirtualTsGenerationOptions {
legacy_vue2: true,
..Default::default()
},
)
}
pub(crate) fn generate_virtual_ts_with_offsets_and_checks(
summary: &Croquis,
script_content: Option<&str>,
template_ast: Option<&vize_relief::ast::RootNode<'_>>,
script_offset: u32,
template_offset: u32,
options: &VirtualTsOptions,
generation_options: VirtualTsGenerationOptions,
) -> VirtualTsOutput {
let check_options = generation_options.check_options;
let legacy_vue2 = generation_options.legacy_vue2;
let options_api = generation_options.options_api || legacy_vue2;
let mut ts = String::default();
let mut mappings: Vec<VizeMapping> = Vec::new();
ts.push_str("/// <reference lib=\"es2022\" />\n");
ts.push_str("/// <reference lib=\"dom\" />\n");
ts.push_str("/// <reference lib=\"dom.iterable\" />\n");
ts.push_str("// ============================================\n");
ts.push_str("// Virtual TypeScript for Vue SFC Type Checking\n");
ts.push_str("// Generated by vize\n");
ts.push_str("// ============================================\n\n");
let (generic_param, mut is_async) = summary
.scopes
.iter()
.find(|s| matches!(s.kind, ScopeKind::ScriptSetup))
.map(|s| {
if let ScopeData::ScriptSetup(data) = s.data() {
(data.generic.as_ref().map(|s| s.as_str()), data.is_async)
} else {
(None, false)
}
})
.unwrap_or((None, false));
if let Some(script) = script_content
&& script.contains("await ")
&& !is_async
{
is_async = true;
}
ts.push_str(IMPORT_META_AUGMENTATION);
ts.push('\n');
ts.push_str("// ========== Module Scope (imports) ==========\n");
ts.push_str(VUE_TYPE_HELPERS);
ts.push('\n');
let module_spans: Vec<(u32, u32)> = profile!("canon.virtual_ts.collect_module_spans", {
let mut module_spans = Vec::new();
for imp in &summary.import_statements {
module_spans.push((imp.start, imp.end));
}
for re in &summary.re_exports {
module_spans.push((re.start, re.end));
}
let has_script_setup = summary
.scopes
.iter()
.any(|scope| matches!(scope.kind, ScopeKind::ScriptSetup));
if has_script_setup {
module_spans.extend(summary.scopes.iter().filter_map(|scope| {
matches!(scope.kind, ScopeKind::NonScriptSetup)
.then_some((scope.span.start, scope.span.end))
}));
}
for te in &summary.type_exports {
if te.hoisted {
module_spans.push((te.start, te.end));
}
}
merge_overlapping_spans(module_spans)
});
let generic_injection: Option<(String, Vec<String>)> = generic_param.map(|g| {
let defaults = add_generic_defaults(g);
let names = extract_generic_names(g)
.split(',')
.map(|n| String::from(n.trim()))
.filter(|n| !n.is_empty())
.collect();
(defaults, names)
});
let hoisted_type_spans: FxHashMap<(u32, u32), &str> = if generic_injection.is_some() {
summary
.type_exports
.iter()
.filter(|te| te.hoisted)
.map(|te| ((te.start, te.end), te.name.as_str()))
.collect()
} else {
FxHashMap::default()
};
if let Some(script) = script_content {
profile!("canon.virtual_ts.emit_module_statements", {
for &(start, end) in &module_spans {
let text = &script[start as usize..end as usize];
if let Some((defaults, names)) = &generic_injection
&& let Some(type_name) = hoisted_type_spans.get(&(start, end))
&& references_any_identifier(text, names)
&& let Some(inject_at) = generic_injection_point(text, type_name)
{
let (prefix, suffix) = text.split_at(inject_at);
let src_base = script_offset as usize + start as usize;
let gen_start = ts.len();
ts.push_str(prefix);
mappings.push(VizeMapping {
gen_range: gen_start..ts.len(),
src_range: src_base..(src_base + prefix.len()),
sub_spans: Vec::new(),
});
append!(ts, "<{defaults}>");
if suffix.starts_with('=') {
ts.push(' ');
}
let gen_start = ts.len();
ts.push_str(suffix);
ts.push('\n');
mappings.push(VizeMapping {
gen_range: gen_start..ts.len(),
src_range: (src_base + prefix.len())
..(src_base + prefix.len() + suffix.len()),
sub_spans: Vec::new(),
});
continue;
}
let gen_start = ts.len();
if text.contains("export default") {
let text = rewrite_export_default_for_module_scope(text);
ts.push_str(&text);
} else {
ts.push_str(text);
}
ts.push('\n');
let gen_end = ts.len();
mappings.push(VizeMapping {
gen_range: gen_start..gen_end,
src_range: (script_offset as usize + start as usize)
..(script_offset as usize + end as usize),
sub_spans: Vec::new(),
});
}
let shadowed_imports: Vec<&&str> = SETUP_SCOPE_HELPER_NAMES
.iter()
.filter(|&&name| summary.bindings.bindings.contains_key(name))
.collect();
if !shadowed_imports.is_empty() {
ts.push_str(
"// Prevent TS6133 for imports shadowed by setup-scope compiler macros\n",
);
for name in &shadowed_imports {
append!(ts, "void {name};\n");
}
}
});
}
if !options.auto_import_stubs.is_empty() {
let imported_names: FxHashSet<&str> = profile!(
"canon.virtual_ts.extract_imported_names",
if let Some(script) = script_content {
summary
.import_statements
.iter()
.flat_map(|imp| {
let text = script
.get(imp.start as usize..imp.end as usize)
.unwrap_or("");
extract_import_names(text)
})
.collect()
} else {
FxHashSet::default()
}
);
profile!("canon.virtual_ts.emit_auto_import_stubs", {
let mut has_header = false;
for stub in &options.auto_import_stubs {
let name = extract_declared_name(stub);
if let Some(name) = name {
if summary.bindings.bindings.contains_key(name)
|| imported_names.contains(&name)
{
continue;
}
}
if !has_header {
ts.push_str("\n// Auto-import stubs (framework-provided globals)\n");
has_header = true;
}
ts.push_str(stub);
ts.push('\n');
}
});
}
ts.push('\n');
profile!(
"canon.virtual_ts.generate_props_type",
generate_props_type(&mut ts, summary, generic_param)
);
ts.push_str("// ========== Setup Scope ==========\n");
let async_prefix = if is_async { "async " } else { "" };
let generic_params = generic_param.map(|g| cstr!("<{g}>")).unwrap_or_default();
append!(ts, "{async_prefix}function __setup{generic_params}() {{\n",);
ts.push_str(VUE_SETUP_HELPERS);
ts.push_str("\n\n");
if let Some(script) = script_content {
profile!("canon.virtual_ts.emit_script_body", {
ts.push_str(" // User setup code\n");
let script_gen_start = ts.len();
let mut src_byte_offset: usize = 0; let mut module_span_index = 0usize;
let uses_import_meta = script.contains("import.meta");
if uses_import_meta {
ts.push_str(" const __import_meta: any = {};\n");
}
for raw_line in script.split('\n') {
let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
let raw_byte_len = raw_line.len() + 1;
let line_start = src_byte_offset;
let line_end = line_start + raw_line.len(); while module_span_index < module_spans.len()
&& module_spans[module_span_index].1 as usize <= line_start
{
module_span_index += 1;
}
let is_module_level = module_spans[module_span_index..]
.iter()
.take_while(|&&(start, _)| (start as usize) < line_end)
.any(|&(start, end)| line_start < end as usize && line_end > start as usize);
if is_module_level {
src_byte_offset += raw_byte_len;
continue;
}
let gen_line_start = ts.len();
ts.push_str(" "); let gen_content_start = ts.len();
let mut output_line = std::borrow::Cow::Borrowed(line);
let trimmed_line = output_line.trim_start();
if let Some(default_expr) = trimmed_line
.strip_prefix("export default")
.filter(|rest| rest.chars().next().is_none_or(char::is_whitespace))
{
let leading_ws = &output_line[..output_line.len() - trimmed_line.len()];
#[allow(clippy::disallowed_types)]
{
output_line = std::borrow::Cow::Owned(
cstr!("{leading_ws}const __default__ ={}", default_expr).into(),
);
}
} else if trimmed_line.starts_with("export ")
&& !trimmed_line.starts_with("export type ")
&& !trimmed_line.starts_with("export interface ")
{
let leading_ws = &output_line[..output_line.len() - trimmed_line.len()];
if let Some(rest) = trimmed_line.strip_prefix("export ") {
#[allow(clippy::disallowed_types)]
{
output_line =
std::borrow::Cow::Owned(cstr!("{leading_ws}{rest}").into());
}
}
}
if uses_import_meta && output_line.contains("import.meta") {
#[allow(clippy::disallowed_types)]
{
output_line = std::borrow::Cow::Owned(
output_line.replace("import.meta", "__import_meta"),
);
}
}
ts.push_str(&output_line);
let gen_content_end = ts.len();
ts.push('\n');
if !line.is_empty() {
let src_line_start = script_offset as usize + src_byte_offset;
let src_line_end = src_line_start + line.len();
mappings.push(VizeMapping {
gen_range: gen_content_start..gen_content_end,
src_range: src_line_start..src_line_end,
sub_spans: Vec::new(),
});
}
let _ = gen_line_start; src_byte_offset += raw_byte_len;
}
let script_gen_end = ts.len();
append!(
ts,
" // @vize-map: {script_gen_start}:{script_gen_end} -> 0:{}\n\n",
script.len()
);
});
}
if template_ast.is_some() {
profile!("canon.virtual_ts.emit_template_scope", {
ts.push_str(" // ========== Template Scope (inherits from setup) ==========\n");
let mut ref_bindings: Vec<&str> = summary
.bindings
.bindings
.iter()
.filter(|(name, binding_type)| {
summary.reactivity.needs_value_access(name.as_str())
|| matches!(binding_type, BindingType::SetupMaybeRef)
&& is_local_setup_binding(summary, name.as_str())
})
.map(|(name, _)| name.as_str())
.collect();
ref_bindings.sort_unstable();
if !ref_bindings.is_empty() {
ts.push_str(" // Ref type captures (before template scope shadows them)\n");
for name in &ref_bindings {
append!(ts, " type __R_{name} = typeof {name};\n");
}
}
ts.push_str(" ;(function __template() {\n");
if !ref_bindings.is_empty() {
ts.push_str(" // Auto-unwrap Vue refs in template scope\n");
ts.push_str(
" type __U<T> = T extends import('vue').Ref<infer V, any> ? V : T;\n",
);
for name in &ref_bindings {
append!(ts, " var {name}: __U<__R_{name}> = undefined as any;\n");
}
}
let template_context = profile!(
"canon.virtual_ts.generate_template_context",
generate_template_context(options)
);
ts.push_str(&template_context);
ts.push('\n');
profile!(
"canon.virtual_ts.generate_props_variables",
generate_props_variables(&mut ts, summary, script_content, generic_param)
);
if options_api {
profile!(
"canon.virtual_ts.generate_options_api_variables",
generate_options_api_variables(&mut ts, summary, options)
);
}
let template_prop_names = profile!(
"canon.virtual_ts.collect_template_prop_names",
collect_template_prop_names(summary, script_content)
);
if check_options.any_enabled() {
profile!(
"canon.virtual_ts.generate_scope_closures",
generate_scope_closures(
&mut ts,
&mut mappings,
summary,
&template_prop_names,
template_offset,
check_options,
options,
)
);
}
if !summary.used_components.is_empty() {
let external_template_bindings: FxHashSet<&str> = options
.external_template_bindings
.iter()
.map(|name| name.as_str())
.collect();
let mut has_unresolved = false;
for component in &summary.used_components {
let name = component.as_str();
if summary.bindings.bindings.contains_key(name)
|| external_template_bindings.contains(name)
{
continue;
}
if !has_unresolved {
ts.push_str(
"\n // Auto-imported/built-in components (not in script bindings)\n",
);
has_unresolved = true;
}
let safe = to_safe_identifier(name);
append!(ts, " const {safe}: any = undefined as any;\n");
}
ts.push_str("\n // Mark used components as referenced\n");
for component in &summary.used_components {
let safe = to_safe_identifier(component.as_str());
append!(ts, " void {safe};\n");
}
}
if !summary.bindings.bindings.is_empty() {
ts.push_str("\n // Reference setup bindings (used in template/CSS v-bind)\n ");
let mut first = true;
let mut binding_names: Vec<&str> = summary
.bindings
.bindings
.keys()
.map(|name| name.as_str())
.collect();
binding_names.sort_unstable();
for name in binding_names {
if matches!(
name,
"default"
| "class"
| "new"
| "delete"
| "void"
| "typeof"
| "in"
| "instanceof"
| "return"
| "switch"
| "case"
| "break"
| "continue"
| "throw"
| "try"
| "catch"
| "finally"
| "if"
| "else"
| "for"
| "while"
| "do"
| "with"
| "var"
| "let"
| "const"
| "function"
| "this"
| "super"
| "import"
| "export"
| "yield"
| "await"
| "async"
| "static"
| "enum"
| "implements"
| "interface"
| "package"
| "private"
| "protected"
| "public"
) {
continue;
}
if !first {
ts.push(' ');
}
append!(ts, "void {name};");
first = false;
}
ts.push('\n');
}
ts.push_str(" })();\n");
});
}
if let Some(destructure) = summary.macros.props_destructure()
&& !destructure.bindings.is_empty()
{
ts.push_str("\n // Reference destructured props (prevent TS6133)\n ");
let mut first = true;
for binding in destructure.bindings.values() {
if !first {
ts.push(' ');
}
append!(ts, "void {};", binding.local);
first = false;
}
if let Some(ref rest) = destructure.rest_id {
if !first {
ts.push(' ');
}
append!(ts, "void {};", rest);
}
ts.push('\n');
}
let define_emits_runtime_args = summary.macros.define_emits().and_then(|call| {
if call.type_args.is_none() {
call.runtime_args.as_ref()
} else {
None
}
});
let mut setup_return_fields = Vec::new();
if let Some(expose) = summary.macros.define_expose()
&& expose.type_args.is_none()
&& let Some(runtime_args) = expose.runtime_args.as_ref()
{
append!(ts, "\n const __vize_exposed = ({runtime_args});\n");
setup_return_fields.push("__vize_exposed");
}
if let Some(runtime_args) = define_emits_runtime_args {
append!(
ts,
"\n const __vize_emits = defineEmits({runtime_args});\n"
);
setup_return_fields.push("__vize_emits");
}
if !setup_return_fields.is_empty() {
append!(ts, "\n return {{ {} }};\n", setup_return_fields.join(", "));
}
ts.push_str("}\n\n");
ts.push_str("// Invoke setup to verify types\n");
ts.push_str("__setup();\n\n");
let emits_already_defined = summary
.type_exports
.iter()
.any(|te| te.name.as_str() == "Emits");
let define_emits_type_args = summary
.macros
.define_emits()
.and_then(|call| call.type_args.as_ref());
let models = summary.macros.models();
let has_model_emits = !models.is_empty();
let has_emits_for_props = emits_already_defined
|| define_emits_type_args.is_some()
|| define_emits_runtime_args.is_some()
|| !summary.macros.emits().is_empty()
|| has_model_emits;
if !emits_already_defined {
if let Some(type_args) = define_emits_type_args {
let inner_type = type_args
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(type_args.as_str());
if has_model_emits {
append!(ts, "export type Emits = {inner_type} & {{\n");
for model in models {
let name = model.name.as_str();
let payload = model.model_type.as_deref().unwrap_or("unknown");
append!(ts, " \"update:{name}\": [value: {payload}];\n");
}
ts.push_str("};\n");
} else {
append!(ts, "export type Emits = {inner_type};\n");
}
} else if define_emits_runtime_args.is_some() {
ts.push_str(
"export type Emits = Awaited<ReturnType<typeof __setup>>[\"__vize_emits\"]",
);
for model in models {
let name = model.name.as_str();
let payload = model.model_type.as_deref().unwrap_or("unknown");
append!(
ts,
" & ((event: \"update:{name}\", value: {payload}) => void)"
);
}
ts.push_str(";\n");
} else if !summary.macros.emits().is_empty() || has_model_emits {
ts.push_str("export type Emits = {\n");
let mut emitted_names: FxHashSet<String> = FxHashSet::default();
for emit in summary.macros.emits() {
let payload = emit.payload_type.as_deref().unwrap_or("any[]");
append!(ts, " \"{}\": {payload};\n", emit.name);
emitted_names.insert(emit.name.as_str().into());
}
for model in models {
let event_name = cstr!("update:{}", model.name);
if emitted_names.contains(event_name.as_str()) {
continue;
}
let payload = model.model_type.as_deref().unwrap_or("unknown");
append!(ts, " \"{event_name}\": [value: {payload}];\n");
}
ts.push_str("};\n");
} else {
ts.push_str("export type Emits = {};\n");
}
}
let slots_type_args = summary
.macros
.define_slots()
.and_then(|m| m.type_args.as_ref());
if let Some(type_args) = slots_type_args {
let inner_type = type_args
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(type_args.as_str());
append!(ts, "export type Slots = {inner_type};\n");
} else {
ts.push_str("export type Slots = {};\n");
}
let has_exposed_type = summary
.macros
.define_expose()
.is_some_and(|expose| expose.type_args.is_some() || expose.runtime_args.is_some());
if let Some(expose) = summary.macros.define_expose() {
if let Some(ref type_args) = expose.type_args {
let inner_type = type_args
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(type_args.as_str());
append!(ts, "export type Exposed = {inner_type};\n");
} else if expose.runtime_args.is_some() {
ts.push_str(
"export type Exposed = Awaited<ReturnType<typeof __setup>>[\"__vize_exposed\"];\n",
);
}
}
ts.push('\n');
if has_emits_for_props {
ts.push_str("type __VizeOverloadProps<TOverload> = Pick<TOverload, keyof TOverload>;\n");
ts.push_str("type __VizeOverloadUnionRecursive<TOverload, TPartialOverload = unknown> = TOverload extends (...args: infer TArgs) => infer TReturn ? TPartialOverload extends TOverload ? never : __VizeOverloadUnionRecursive<TPartialOverload & TOverload, TPartialOverload & ((...args: TArgs) => TReturn) & __VizeOverloadProps<TOverload>> | ((...args: TArgs) => TReturn) : never;\n");
ts.push_str("type __VizeOverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<__VizeOverloadUnionRecursive<(() => never) & TOverload>, TOverload extends () => never ? never : () => never>;\n");
ts.push_str("type __VizeOverloadParameters<T extends (...args: any[]) => any> = Parameters<__VizeOverloadUnion<T>>;\n");
ts.push_str("type __VizeIsStringLiteral<T> = T extends string ? string extends T ? false : true : false;\n");
ts.push_str("type __VizeParametersToFns<T extends any[]> = { [K in T[0]]: __VizeIsStringLiteral<K> extends true ? (...args: T extends [e: infer E, ...args: infer P] ? K extends E ? P : never : never) => any : never };\n");
ts.push_str("type __EmitOptions<T> = { [K in keyof __EmitShape<T> & string]: (...args: __EmitArgs<__EmitShape<T>, K>) => any } & (__EmitShape<T> extends (...args: any[]) => any ? __VizeParametersToFns<__VizeOverloadParameters<__EmitShape<T>>> : {});\n");
ts.push_str("type __EmitProps<T> = import('vue').EmitsToProps<__EmitOptions<T>>;\n\n");
}
ts.push_str("// ========== Default Export ==========\n");
ts.push_str("type __VizeComponentInstance = {\n");
if has_emits_for_props {
ts.push_str(" $props: Props & __EmitProps<Emits>;\n");
} else {
ts.push_str(" $props: Props;\n");
}
ts.push_str(" $emit: __EmitFn<Emits>;\n");
ts.push_str(" $slots: Slots;\n");
if has_exposed_type {
ts.push_str("} & Exposed;\n");
} else {
ts.push_str("};\n");
}
if let Some(generic) = generic_param {
let generic_decl = add_generic_defaults(generic);
let generic_names = extract_generic_names(generic);
append!(
ts,
"declare const __vize_component__: (new (...args: any[]) => __VizeComponentInstance) & {{ __vizeCheck: <{generic_decl}>(props: Partial<Props<{generic_names}>> & Record<string, unknown>) => void; }};\n",
);
} else {
ts.push_str(
"declare const __vize_component__: new (...args: any[]) => __VizeComponentInstance;\n",
);
}
ts.push_str("export default __vize_component__;\n");
VirtualTsOutput { code: ts, mappings }
}
fn merge_overlapping_spans(mut spans: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
spans.retain(|(start, end)| start < end);
spans.sort_by_key(|&(start, end)| (start, end));
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(spans.len());
for (start, end) in spans {
if let Some((_, previous_end)) = merged.last_mut()
&& start <= *previous_end
{
*previous_end = (*previous_end).max(end);
continue;
}
merged.push((start, end));
}
merged
}
fn rewrite_export_default_for_module_scope(text: &str) -> String {
let mut output = String::with_capacity(text.len());
for segment in text.split_inclusive('\n') {
let (line_with_optional_cr, newline) = segment
.strip_suffix('\n')
.map_or((segment, ""), |line| (line, "\n"));
let (line, carriage_return) = line_with_optional_cr
.strip_suffix('\r')
.map_or((line_with_optional_cr, ""), |line| (line, "\r"));
let trimmed_line = line.trim_start();
if let Some(default_expr) = trimmed_line
.strip_prefix("export default")
.filter(|rest| rest.chars().next().is_none_or(char::is_whitespace))
{
let leading_ws = &line[..line.len() - trimmed_line.len()];
append!(output, "{leading_ws}const __default__ ={default_expr}");
} else {
output.push_str(line);
}
output.push_str(carriage_return);
output.push_str(newline);
}
output
}
fn extract_declared_name(stub: &str) -> Option<&str> {
for prefix in [
"declare function ",
"declare const ",
"declare let ",
"declare var ",
] {
let Some(rest) = stub.strip_prefix(prefix) else {
continue;
};
let end = rest
.find(['<', '(', ':', '=', ';', ' '])
.unwrap_or(rest.len());
let name = rest[..end].trim();
if !name.is_empty() {
return Some(name);
}
}
None
}
fn extract_import_names(import_text: &str) -> Vec<&str> {
let mut names = Vec::new();
if let Some(brace_start) = import_text.find('{') {
if let Some(brace_end) = import_text.find('}') {
let inner = &import_text[brace_start + 1..brace_end];
for part in inner.split(',') {
let part = part.trim();
if part.is_empty() || part.starts_with("//") || part.starts_with("type ") {
continue;
}
if let Some(as_pos) = part.find(" as ") {
let alias = part[as_pos + 4..].trim();
if !alias.is_empty() {
names.push(alias);
}
} else {
let name = part.strip_suffix('\r').unwrap_or(part);
if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
names.push(name);
}
}
}
}
} else {
let text = import_text.trim();
if let Some(rest) = text.strip_prefix("import ")
&& let Some(from_pos) = rest.find(" from ")
{
let name = rest[..from_pos].trim();
if !name.is_empty()
&& !name.contains('{')
&& !name.contains('*')
&& name.chars().all(|c| c.is_alphanumeric() || c == '_')
{
names.push(name);
}
}
}
names
}
fn is_local_setup_binding(summary: &Croquis, name: &str) -> bool {
let Some(&(start, end)) = summary.binding_spans.get(name) else {
return true;
};
!summary
.import_statements
.iter()
.any(|import| start >= import.start && end <= import.end)
}
fn generate_options_api_variables(
mut ts: &mut String,
summary: &Croquis,
options: &VirtualTsOptions,
) {
let macro_prop_names: FxHashSet<&str> = summary
.macros
.props()
.iter()
.map(|prop| prop.name.as_str())
.collect();
let configured_globals: FxHashSet<&str> = options
.template_globals
.iter()
.map(|global| global.name.as_str())
.collect();
let mut names: Vec<&str> = summary
.bindings
.bindings
.iter()
.filter_map(|(name, binding_type)| {
let name = name.as_str();
match binding_type {
BindingType::Data | BindingType::Options | BindingType::VueGlobal => Some(name),
BindingType::Props if !macro_prop_names.contains(name) => Some(name),
_ => None,
}
})
.filter(|name| !configured_globals.contains(name))
.filter(|name| is_safe_value_identifier(name))
.collect();
names.sort_unstable();
names.dedup();
if names.is_empty() {
return;
}
ts.push_str(" // Options API template bindings\n");
for name in &names {
append!(ts, " const {name}: any = undefined as any;\n");
}
ts.push_str(" ");
for name in &names {
append!(ts, "void {name};");
}
ts.push('\n');
}
fn is_safe_value_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_' || first == '$') {
return false;
}
chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
}
fn is_ident_byte(b: u8) -> bool {
b == b'_' || b == b'$' || b.is_ascii_alphanumeric()
}
fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
i
}
fn references_any_identifier(haystack: &str, idents: &[String]) -> bool {
let bytes = haystack.as_bytes();
idents.iter().any(|ident| {
let ident = ident.as_str();
if ident.is_empty() {
return false;
}
let mut from = 0;
while let Some(rel) = haystack[from..].find(ident) {
let at = from + rel;
let before_ok = at == 0 || !is_ident_byte(bytes[at - 1]);
let after = at + ident.len();
let after_ok = after >= bytes.len() || !is_ident_byte(bytes[after]);
if before_ok && after_ok {
return true;
}
from = at + ident.len();
}
false
})
}
fn generic_injection_point(decl: &str, type_name: &str) -> Option<usize> {
let bytes = decl.as_bytes();
let mut i = skip_ascii_ws(bytes, 0);
if decl[i..].starts_with("export")
&& matches!(bytes.get(i + 6), Some(b) if b.is_ascii_whitespace())
{
i = skip_ascii_ws(bytes, i + 6);
}
if decl[i..].starts_with("type")
&& matches!(bytes.get(i + 4), Some(b) if b.is_ascii_whitespace())
{
i += 4;
} else if decl[i..].starts_with("interface")
&& matches!(bytes.get(i + 9), Some(b) if b.is_ascii_whitespace())
{
i += 9;
} else {
return None;
}
i = skip_ascii_ws(bytes, i);
if !decl[i..].starts_with(type_name) {
return None;
}
let name_end = i + type_name.len();
if matches!(bytes.get(name_end), Some(&b) if is_ident_byte(b)) {
return None;
}
if bytes.get(skip_ascii_ws(bytes, name_end)) == Some(&b'<') {
return None;
}
Some(name_end)
}