#[cfg(feature = "swc")]
use swc_core::common::{FileName, SourceMap, sync::Lrc};
#[cfg(feature = "swc")]
use swc_core::ecma::ast::*;
#[cfg(feature = "swc")]
use swc_core::ecma::codegen::{Config, Emitter, text_writer::JsWriter};
#[cfg(feature = "swc")]
use swc_core::ecma::parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer};
use crate::TsSynError;
#[derive(Debug, Clone, Copy)]
pub struct ImportConfig {
pub name: &'static str,
pub alias: &'static str,
pub module: &'static str,
pub is_type: bool,
}
impl ImportConfig {
pub const fn value(name: &'static str, alias: &'static str, module: &'static str) -> Self {
Self {
name,
alias,
module,
is_type: false,
}
}
pub const fn type_only(name: &'static str, alias: &'static str, module: &'static str) -> Self {
Self {
name,
alias,
module,
is_type: true,
}
}
}
pub struct TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc<swc_core::common::SourceMap>,
source: String,
file_name: String,
pub ctx: Option<crate::abi::MacroContextIR>,
pub runtime_patches: Vec<crate::abi::Patch>,
pub insert_pos: crate::abi::InsertPos,
pub cross_module_suffixes: Vec<String>,
pub cross_module_type_suffixes: Vec<String>,
}
#[cfg(feature = "oxc")]
impl TsStream {
pub fn parse_stmt_oxc<'a>(
&'a self,
allocator: &'a oxc::allocator::Allocator,
) -> Result<oxc::ast::ast::Statement<'a>, TsSynError> {
let source_type = oxc::span::SourceType::ts()
.with_typescript(true)
.with_jsx(self.file_name.ends_with(".tsx"));
let ret = oxc::parser::Parser::new(allocator, &self.source, source_type).parse();
if !ret.errors.is_empty() {
return Err(TsSynError::Parse(format!(
"Oxc parse errors: {:?}",
ret.errors
)));
}
ret.program
.body
.into_iter()
.next()
.ok_or_else(|| TsSynError::Parse("No statement found".to_string()))
}
}
#[cfg(feature = "swc")]
pub fn format_ts_source(source: &str) -> String {
let cm = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(FileName::Custom("fmt.ts".into()).into(), source.to_string());
let syntax = Syntax::Typescript(TsSyntax {
tsx: true,
decorators: true,
..Default::default()
});
let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None);
let mut parser = Parser::new_from(lexer);
if let Ok(module) = parser.parse_module() {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Config::default().with_minify(false),
cm: cm.clone(),
comments: None,
wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
};
if emitter.emit_module(&module).is_ok() {
return String::from_utf8(buf).unwrap_or_else(|_| source.to_string());
}
}
}
let wrapped_source = format!("class __FmtWrapper {{ {} }}", source);
let fm_wrapped = cm.new_source_file(
FileName::Custom("fmt_wrapped.ts".into()).into(),
wrapped_source,
);
let lexer = Lexer::new(
syntax,
EsVersion::latest(),
StringInput::from(&*fm_wrapped),
None,
);
let mut parser = Parser::new_from(lexer);
if let Ok(module) = parser.parse_module() {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Config::default().with_minify(false),
cm: cm.clone(),
comments: None,
wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
};
if emitter.emit_module(&module).is_ok() {
let full_output = String::from_utf8(buf).unwrap_or_default();
if let (Some(start), Some(end)) = (full_output.find('{'), full_output.rfind('}')) {
let content = &full_output[start + 1..end];
let lines: Vec<&str> = content.lines().collect();
let mut result = String::new();
for line in lines {
let trimmed = line.strip_prefix(" ").unwrap_or(line);
if !trimmed.trim().is_empty() {
result.push_str(trimmed);
result.push('\n');
}
}
return result.trim().to_string();
}
}
}
}
source.to_string()
}
impl TsStream {
pub fn new(source: &str, file_name: &str) -> Result<Self, TsSynError> {
Ok(TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc::new(Default::default()),
source: source.to_string(),
file_name: file_name.to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
})
}
pub fn from_string(source: String) -> Self {
TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn with_insert_pos(source: String, insert_pos: crate::abi::InsertPos) -> Self {
TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos,
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn with_insert_pos_and_patches(
source: String,
insert_pos: crate::abi::InsertPos,
runtime_patches: Vec<crate::abi::Patch>,
) -> Self {
TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches,
insert_pos,
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn source(&self) -> &str {
&self.source
}
pub fn with_context(
source: &str,
file_name: &str,
ctx: crate::abi::MacroContextIR,
) -> Result<Self, TsSynError> {
Ok(TsStream {
#[cfg(feature = "swc")]
source_map: swc_core::common::sync::Lrc::new(Default::default()),
source: source.to_string(),
file_name: file_name.to_string(),
ctx: Some(ctx),
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
})
}
pub fn context(&self) -> Option<&crate::abi::MacroContextIR> {
self.ctx.as_ref()
}
pub fn into_result(self) -> crate::abi::MacroResult {
let imports = crate::import_registry::with_registry_mut(|r| r.take_generated_imports());
crate::abi::MacroResult {
runtime_patches: self.runtime_patches,
type_patches: vec![],
diagnostics: vec![],
tokens: Some(self.source),
insert_pos: self.insert_pos,
debug: None,
cross_module_suffixes: self.cross_module_suffixes,
cross_module_type_suffixes: self.cross_module_type_suffixes,
imports,
}
}
pub fn add_import(&mut self, specifier: &str, module: &str) {
let (original, local) = parse_import_specifier(specifier);
crate::import_registry::with_registry_mut(|r| {
r.request_import(&local, original.as_deref(), module, false);
});
}
pub fn add_type_import(&mut self, specifier: &str, module: &str) {
let (original, local) = parse_import_specifier(specifier);
crate::import_registry::with_registry_mut(|r| {
r.request_import(&local, original.as_deref(), module, true);
});
}
pub fn module_specifier_for(&self, type_name: &str) -> Option<String> {
if let Some(ctx) = self.ctx.as_ref() {
return ctx.import_specifier_for(type_name);
}
crate::context_registry::with_context(|ctx| {
ctx.and_then(|c| c.import_specifier_for(type_name))
})
}
pub fn add_import_for(&mut self, local_name: &str, type_name: &str) -> bool {
let Some(module) = self.module_specifier_for(type_name) else {
return false;
};
self.add_import(local_name, &module);
true
}
pub fn add_type_import_for(&mut self, local_name: &str, type_name: &str) -> bool {
let Some(module) = self.module_specifier_for(type_name) else {
return false;
};
self.add_type_import(local_name, &module);
true
}
pub fn add_helpers_for(&mut self, type_name: &str, helpers: &[(&str, bool)]) -> bool {
let Some(module) = self.module_specifier_for(type_name) else {
return false;
};
for (name, is_type) in helpers {
if *is_type {
self.add_type_import(name, &module);
} else {
self.add_import(name, &module);
}
}
true
}
pub fn add_aliased_import(&mut self, name: &str, module: &str) {
let alias = format!("__mf_{name}");
crate::import_registry::with_registry_mut(|r| {
r.request_import(&alias, Some(name), module, false);
});
}
pub fn add_aliased_type_import(&mut self, name: &str, module: &str) {
let alias = format!("__mf_{name}");
crate::import_registry::with_registry_mut(|r| {
r.request_import(&alias, Some(name), module, true);
});
}
pub fn add_import_as(&mut self, name: &str, alias: &str, module: &str) {
crate::import_registry::with_registry_mut(|r| {
r.request_import(alias, Some(name), module, false);
});
}
pub fn add_type_import_as(&mut self, name: &str, alias: &str, module: &str) {
crate::import_registry::with_registry_mut(|r| {
r.request_import(alias, Some(name), module, true);
});
}
pub fn add_imports(&mut self, imports: &[ImportConfig]) {
crate::import_registry::with_registry_mut(|r| {
for import in imports {
r.request_import(
import.alias,
Some(import.name),
import.module,
import.is_type,
);
}
});
}
pub fn add_cross_module_suffix(&mut self, suffix: &str) {
self.cross_module_suffixes.push(suffix.to_string());
}
pub fn add_cross_module_type_suffix(&mut self, suffix: &str) {
self.cross_module_type_suffixes.push(suffix.to_string());
}
pub fn merge(mut self, other: Self) -> Self {
if !self.source.is_empty() && !other.source.is_empty() {
let left_ends_ws = self
.source
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(false);
let right_starts_ws = other
.source
.chars()
.next()
.map(|c| c.is_whitespace())
.unwrap_or(false);
if !(left_ends_ws || right_starts_ws) {
self.source.push('\n');
}
}
self.source.push_str(&other.source);
self.runtime_patches.extend(other.runtime_patches);
self.cross_module_suffixes
.extend(other.cross_module_suffixes);
self.cross_module_type_suffixes
.extend(other.cross_module_type_suffixes);
self
}
pub fn merge_all(streams: impl IntoIterator<Item = Self>) -> Self {
let mut iter = streams.into_iter();
match iter.next() {
Some(first) => iter.fold(first, |acc, stream| acc.merge(stream)),
None => Self::from_string(String::new()),
}
}
#[cfg(feature = "swc")]
fn with_parser<F, T>(&self, f: F) -> Result<T, TsSynError>
where
F: for<'a> FnOnce(
&mut swc_core::ecma::parser::Parser<swc_core::ecma::parser::lexer::Lexer<'a>>,
) -> swc_core::ecma::parser::PResult<T>,
{
let fm = self.source_map.new_source_file(
swc_core::common::FileName::Custom(self.file_name.clone()).into(),
self.source.clone(),
);
let syntax = swc_core::ecma::parser::Syntax::Typescript(swc_core::ecma::parser::TsSyntax {
tsx: self.file_name.ends_with(".tsx"),
decorators: true,
..Default::default()
});
let lexer = swc_core::ecma::parser::lexer::Lexer::new(
syntax,
swc_core::ecma::ast::EsVersion::latest(),
swc_core::ecma::parser::StringInput::from(&*fm),
None,
);
let mut parser = swc_core::ecma::parser::Parser::new_from(lexer);
f(&mut parser).map_err(|e| TsSynError::Parse(format!("{:?}", e)))
}
#[cfg(feature = "swc")]
pub fn parse<T: ParseTs>(&mut self) -> Result<T, TsSynError> {
T::parse(self)
}
#[cfg(feature = "swc")]
pub fn parse_ident(&self) -> Result<swc_core::ecma::ast::Ident, TsSynError> {
self.with_parser(|parser| {
use swc_core::common::DUMMY_SP;
use swc_core::ecma::parser::error::{Error, SyntaxError};
parser.parse_expr().and_then(|expr| match *expr {
swc_core::ecma::ast::Expr::Ident(ident) => Ok(ident),
_ => Err(Error::new(DUMMY_SP, SyntaxError::TS1003)),
})
})
}
#[cfg(feature = "swc")]
pub fn parse_stmt(&self) -> Result<swc_core::ecma::ast::Stmt, TsSynError> {
self.with_parser(|parser| parser.parse_stmt_list_item())
}
#[cfg(feature = "swc")]
pub fn parse_expr(&self) -> Result<Box<swc_core::ecma::ast::Expr>, TsSynError> {
self.with_parser(|parser| parser.parse_expr())
}
#[cfg(feature = "swc")]
pub fn parse_module(&self) -> Result<swc_core::ecma::ast::Module, TsSynError> {
self.with_parser(|parser| parser.parse_module())
}
}
pub trait ParseTs: Sized {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError>;
}
#[cfg(feature = "swc")]
impl ParseTs for Ident {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_ident()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Stmt {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_stmt()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Box<Expr> {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_expr()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Module {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_module()
}
}
#[cfg(feature = "swc")]
pub fn parse_ts_str<T: ParseTs>(code: &str) -> Result<T, TsSynError> {
let mut stream = TsStream::new(code, "input.ts")?;
stream.parse()
}
#[cfg(feature = "swc")]
pub fn parse_ts_expr(code: &str) -> Result<Box<Expr>, TsSynError> {
parse_ts_str(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_stmt(code: &str) -> Result<Stmt, TsSynError> {
parse_ts_str(code)
}
#[cfg(all(feature = "oxc", not(feature = "swc")))]
pub fn parse_ts_stmt(code: &str) -> Result<oxc::ast::ast::Statement<'static>, TsSynError> {
crate::parse_oxc_statement(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_module(code: &str) -> Result<Module, TsSynError> {
parse_ts_str(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_type(code: &str) -> Result<TsType, TsSynError> {
use swc_core::ecma::ast::{Decl, ModuleItem, Stmt};
let wrapped = format!("type __T = {};", code);
let module: Module = parse_ts_str(&wrapped)?;
for item in module.body {
if let ModuleItem::Stmt(Stmt::Decl(Decl::TsTypeAlias(alias))) = item {
return Ok(*alias.type_ann);
}
}
Err(TsSynError::Parse(format!("Failed to parse type: {}", code)))
}
fn parse_import_specifier(specifier: &str) -> (Option<String>, String) {
if let Some(idx) = specifier.find(" as ") {
let original = specifier[..idx].trim().to_string();
let local = specifier[idx + 4..].trim().to_string();
(Some(original), local)
} else {
(None, specifier.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "swc")]
#[test]
fn test_parse_ident() {
let result: Result<Ident, _> = parse_ts_str("myVariable");
assert!(result.is_ok());
assert_eq!(result.unwrap().sym.as_ref(), "myVariable");
}
#[cfg(feature = "swc")]
#[test]
fn test_parse_expr() {
let result = parse_ts_expr("1 + 2");
assert!(result.is_ok());
}
#[cfg(feature = "swc")]
#[test]
fn test_parse_stmt() {
let result = parse_ts_stmt("const x = 5;");
assert!(result.is_ok(), "parse_ts_stmt failed: {:?}", result.err());
}
#[test]
fn test_add_cross_module_suffix() {
let mut stream = TsStream::from_string("export function foo() {}".to_string());
assert!(stream.cross_module_suffixes.is_empty());
stream.add_cross_module_suffix("GetFields");
assert_eq!(stream.cross_module_suffixes, vec!["GetFields"]);
stream.add_cross_module_suffix("CustomSuffix");
assert_eq!(
stream.cross_module_suffixes,
vec!["GetFields", "CustomSuffix"]
);
}
#[test]
fn test_cross_module_suffixes_propagate_to_result() {
let mut stream = TsStream::from_string("export function foo() {}".to_string());
stream.add_cross_module_suffix("GetFields");
stream.add_cross_module_suffix("OtherSuffix");
let result = stream.into_result();
assert_eq!(
result.cross_module_suffixes,
vec!["GetFields", "OtherSuffix"]
);
}
#[test]
fn test_merge_combines_cross_module_suffixes() {
let mut a = TsStream::from_string("export function a() {}".to_string());
a.add_cross_module_suffix("GetFields");
let mut b = TsStream::from_string("export function b() {}".to_string());
b.add_cross_module_suffix("CustomSuffix");
let merged = a.merge(b);
assert_eq!(
merged.cross_module_suffixes,
vec!["GetFields", "CustomSuffix"]
);
}
#[test]
fn test_empty_cross_module_suffixes_by_default() {
let stream = TsStream::from_string("const x = 1;".to_string());
assert!(stream.cross_module_suffixes.is_empty());
let result = stream.into_result();
assert!(result.cross_module_suffixes.is_empty());
}
}
#[cfg(test)]
mod import_for_tests {
use super::*;
use crate::abi::ir::context::{MacroContextIR, MacroKind, TargetIR};
use crate::abi::ir::interface::InterfaceIR;
use crate::abi::ir::type_registry::{TypeDefinitionIR, TypeRegistry, TypeRegistryEntry};
use crate::import_registry::{ImportRegistry, take_registry};
use crate::{SpanIR, context_registry};
fn empty_interface(name: &str) -> InterfaceIR {
InterfaceIR {
name: name.to_string(),
span: SpanIR::new(0, 0),
body_span: SpanIR::new(0, 0),
type_params: vec![],
heritage: vec![],
fields: vec![],
methods: vec![],
decorators: vec![],
}
}
fn make_ctx_with_registry(file_name: &str, type_name: &str, type_path: &str) -> MacroContextIR {
let mut registry = TypeRegistry {
types: std::collections::HashMap::new(),
qualified_types: std::collections::HashMap::new(),
ambiguous_names: vec![],
};
registry.types.insert(
type_name.to_string(),
TypeRegistryEntry {
name: type_name.to_string(),
file_path: type_path.to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(empty_interface(type_name)),
file_imports: vec![],
},
);
MacroContextIR {
abi_version: 1,
macro_kind: MacroKind::Derive,
macro_name: "Test".to_string(),
module_path: "@test".to_string(),
decorator_span: SpanIR::new(0, 0),
macro_name_span: None,
target_span: SpanIR::new(0, 0),
file_name: file_name.to_string(),
target: TargetIR::Interface(empty_interface("Probe")),
target_source: String::new(),
import_registry: ImportRegistry::new(),
config: None,
type_registry: Some(registry),
resolved_fields: None,
}
}
fn reset_thread_locals() {
context_registry::clear_context();
let _ = take_registry();
crate::import_registry::install_registry(ImportRegistry::new());
}
#[test]
fn add_import_for_resolves_via_thread_local_context() {
reset_thread_locals();
let ctx = make_ctx_with_registry(
"/proj/src/order.svelte.ts",
"Customer",
"/proj/src/customer.svelte.ts",
);
context_registry::install_context(ctx);
let mut stream = TsStream::from_string(String::new());
let added = stream.add_import_for("customerDefaultValue", "Customer");
assert!(added);
let result = stream.into_result();
let customer = result
.imports
.iter()
.find(|i| i.local_name == "customerDefaultValue")
.expect("expected customerDefaultValue import to be queued");
assert_eq!(customer.source_module, "./customer.svelte");
assert!(!customer.is_type_only);
context_registry::clear_context();
}
#[test]
fn add_helpers_for_batches_correctly() {
reset_thread_locals();
let ctx = make_ctx_with_registry(
"/proj/src/order.svelte.ts",
"Customer",
"/proj/src/customer.svelte.ts",
);
context_registry::install_context(ctx);
let mut stream = TsStream::from_string(String::new());
let added = stream.add_helpers_for(
"Customer",
&[
("customerDefaultValue", false),
("CustomerErrors", true),
("customerHasShape", false),
],
);
assert!(added);
let result = stream.into_result();
let value = result
.imports
.iter()
.find(|i| i.local_name == "customerDefaultValue")
.expect("value import missing");
assert_eq!(value.source_module, "./customer.svelte");
assert!(!value.is_type_only);
let type_import = result
.imports
.iter()
.find(|i| i.local_name == "CustomerErrors")
.expect("type import missing");
assert_eq!(type_import.source_module, "./customer.svelte");
assert!(type_import.is_type_only);
let has_shape = result
.imports
.iter()
.find(|i| i.local_name == "customerHasShape")
.expect("has_shape import missing");
assert_eq!(has_shape.source_module, "./customer.svelte");
assert!(!has_shape.is_type_only);
context_registry::clear_context();
}
#[test]
fn add_import_for_returns_false_without_context() {
reset_thread_locals();
let mut stream = TsStream::from_string(String::new());
let added = stream.add_import_for("customerDefaultValue", "Customer");
assert!(!added);
let result = stream.into_result();
assert!(result.imports.is_empty());
}
#[test]
fn add_import_for_returns_false_for_co_located_type() {
reset_thread_locals();
let ctx = make_ctx_with_registry(
"/proj/src/customer.svelte.ts",
"Customer",
"/proj/src/customer.svelte.ts",
);
context_registry::install_context(ctx);
let mut stream = TsStream::from_string(String::new());
let added = stream.add_import_for("customerDefaultValue", "Customer");
assert!(!added);
let result = stream.into_result();
assert!(result.imports.is_empty());
context_registry::clear_context();
}
#[test]
fn add_helpers_for_emits_bare_variant_type_import() {
reset_thread_locals();
let ctx = make_ctx_with_registry(
"/proj/src/employee.svelte.ts",
"LinkedUser",
"/proj/src/linked-user.svelte.ts",
);
context_registry::install_context(ctx);
crate::import_registry::with_registry_mut(|r| {
r.install_source_imports(vec![crate::import_registry::SourceImportEntry {
local_name: "LinkedUser".to_string(),
source_module: "./linked-user.svelte".to_string(),
original_name: None,
is_type_only: true,
}]);
});
let mut stream = TsStream::from_string(String::new());
let added = stream.add_helpers_for(
"LinkedUser",
&[
("LinkedUserFieldControllers", true),
("LinkedUser", true),
("linkedUserGetControllers", false),
("linkedUserDefaultErrors", false),
("linkedUserDefaultTainted", false),
("linkedUserDefaultValue", false),
("linkedUserIs", false),
],
);
assert!(added, "add_helpers_for should have resolved LinkedUser");
let result = stream.into_result();
let field_controllers = result
.imports
.iter()
.find(|i| i.local_name == "LinkedUserFieldControllers")
.expect("LinkedUserFieldControllers type import missing");
assert_eq!(field_controllers.source_module, "./linked-user.svelte");
assert!(field_controllers.is_type_only);
let bare = result.imports.iter().find(|i| i.local_name == "LinkedUser");
assert!(
bare.is_none(),
"bare LinkedUser should be dedup'd against source_imports — \
callers must NOT pre-register variant types in source_imports \
if they also want a generated `import type {{ LinkedUser }}` line"
);
for name in [
"linkedUserGetControllers",
"linkedUserDefaultErrors",
"linkedUserDefaultTainted",
"linkedUserDefaultValue",
"linkedUserIs",
] {
let entry = result
.imports
.iter()
.find(|i| i.local_name == name)
.unwrap_or_else(|| panic!("{name} value import missing"));
assert_eq!(entry.source_module, "./linked-user.svelte");
assert!(!entry.is_type_only);
}
context_registry::clear_context();
}
#[test]
fn add_helpers_for_emits_bare_type_when_source_imports_clean() {
reset_thread_locals();
let ctx = make_ctx_with_registry(
"/proj/src/entry.svelte.ts",
"CustomerReferral",
"/proj/src/customer-referral.svelte.ts",
);
context_registry::install_context(ctx);
let mut stream = TsStream::from_string(String::new());
let added = stream.add_helpers_for(
"CustomerReferral",
&[
("CustomerReferralFieldControllers", true),
("CustomerReferral", true),
("customerReferralIs", false),
],
);
assert!(added);
let result = stream.into_result();
let bare = result
.imports
.iter()
.find(|i| i.local_name == "CustomerReferral")
.expect("bare CustomerReferral type import missing");
assert_eq!(bare.source_module, "./customer-referral.svelte");
assert!(bare.is_type_only);
context_registry::clear_context();
}
}