1use regex::Regex;
2use std::collections::hash_map::DefaultHasher;
3use std::hash::{Hash, Hasher};
4
5#[derive(Debug, Clone, PartialEq)]
7pub struct ScriptImport {
8 pub raw: String,
10 pub is_type_only: bool,
12 pub path: String,
14}
15
16pub fn parse_script_imports(script_setup: &str) -> Vec<ScriptImport> {
21 let re = Regex::new(r#"(?m)^[ \t]*(import\s+(?:type\s+)?.*?\s+from\s+['"]([^'"]+\.(?:ts|js|tsx|jsx))['"].*)"#).unwrap();
22 let type_re = Regex::new(r#"^import\s+type\s"#).unwrap();
23 re.captures_iter(script_setup)
24 .map(|cap| {
25 let raw = cap[1].trim().to_string();
26 let path = cap[2].to_string();
27 let is_type_only = type_re.is_match(&raw);
28 ScriptImport {
29 raw,
30 is_type_only,
31 path,
32 }
33 })
34 .collect()
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct VanImport {
40 pub name: String,
42 pub tag_name: String,
44 pub path: String,
46}
47
48pub fn parse_imports(script_setup: &str) -> Vec<VanImport> {
51 let re = Regex::new(r#"import\s+(\w+)\s+from\s+['"]([^'"]+\.van)['"]"#).unwrap();
52 re.captures_iter(script_setup)
53 .map(|cap| {
54 let name = cap[1].to_string();
55 let tag_name = pascal_to_kebab(&name);
56 let path = cap[2].to_string();
57 VanImport {
58 name,
59 tag_name,
60 path,
61 }
62 })
63 .collect()
64}
65
66pub fn pascal_to_kebab(s: &str) -> String {
68 let mut result = String::with_capacity(s.len() + 4);
69 for (i, ch) in s.chars().enumerate() {
70 if ch.is_uppercase() {
71 if i > 0 {
72 result.push('-');
73 }
74 result.push(ch.to_lowercase().next().unwrap());
75 } else {
76 result.push(ch);
77 }
78 }
79 result
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct PropDef {
85 pub name: String,
86 pub prop_type: Option<String>,
88 pub required: bool,
89}
90
91#[derive(Debug, Default)]
93pub struct VanBlock {
94 pub template: Option<String>,
95 pub script_setup: Option<String>,
96 pub script_server: Option<String>,
97 pub style: Option<String>,
98 pub style_scoped: bool,
99 pub props: Vec<PropDef>,
100}
101
102pub fn parse_blocks(source: &str) -> VanBlock {
108 let (style, style_scoped) = extract_style(source);
109 let script_setup = extract_script_setup(source);
110 let props = if let Some(ref script) = script_setup {
111 parse_define_props(script)
112 } else {
113 Vec::new()
114 };
115 VanBlock {
116 template: extract_block(source, "template"),
117 script_setup,
118 script_server: extract_script_server(source),
119 style,
120 style_scoped,
121 props,
122 }
123}
124
125pub fn parse_define_props(script: &str) -> Vec<PropDef> {
131 let Some(start) = script.find("defineProps(") else {
133 return Vec::new();
134 };
135 let after_paren = start + "defineProps(".len();
136 let rest = &script[after_paren..];
137
138 let Some(inner) = extract_balanced_braces(rest) else {
140 return Vec::new();
141 };
142
143 if inner.trim().is_empty() {
144 return Vec::new();
145 }
146
147 let mut props = Vec::new();
148
149 let entries = split_respecting_braces(inner);
151
152 for entry in entries {
153 let entry = entry.trim();
154 if entry.is_empty() {
155 continue;
156 }
157
158 let Some(colon_pos) = entry.find(':') else {
160 continue;
161 };
162 let name = entry[..colon_pos].trim().trim_matches('\'').trim_matches('"').to_string();
163 let value = entry[colon_pos + 1..].trim();
164
165 if value.starts_with('{') {
166 let obj_inner = value
168 .strip_prefix('{')
169 .and_then(|s| s.strip_suffix('}'))
170 .unwrap_or(value)
171 .trim();
172
173 let mut prop_type = None;
174 let mut required = false;
175
176 for part in obj_inner.split(',') {
177 let part = part.trim();
178 if let Some(cp) = part.find(':') {
179 let key = part[..cp].trim();
180 let val = part[cp + 1..].trim();
181 if key == "type" {
182 prop_type = Some(val.to_string());
183 } else if key == "required" {
184 required = val == "true";
185 }
186 }
187 }
188
189 props.push(PropDef {
190 name,
191 prop_type,
192 required,
193 });
194 } else {
195 props.push(PropDef {
197 name,
198 prop_type: Some(value.to_string()),
199 required: false,
200 });
201 }
202 }
203
204 props
205}
206
207fn extract_balanced_braces(s: &str) -> Option<&str> {
209 let s = s.trim();
210 if !s.starts_with('{') {
211 return None;
212 }
213 let mut depth = 0;
214 for (i, ch) in s.char_indices() {
215 match ch {
216 '{' => depth += 1,
217 '}' => {
218 depth -= 1;
219 if depth == 0 {
220 return Some(&s[1..i]);
221 }
222 }
223 _ => {}
224 }
225 }
226 None
227}
228
229fn split_respecting_braces(s: &str) -> Vec<&str> {
231 let mut result = Vec::new();
232 let mut depth = 0;
233 let mut start = 0;
234 for (i, ch) in s.char_indices() {
235 match ch {
236 '{' => depth += 1,
237 '}' => depth -= 1,
238 ',' if depth == 0 => {
239 result.push(&s[start..i]);
240 start = i + 1;
241 }
242 _ => {}
243 }
244 }
245 let tail = &s[start..];
246 if !tail.trim().is_empty() {
247 result.push(tail);
248 }
249 result
250}
251
252fn extract_block(source: &str, tag: &str) -> Option<String> {
253 let open = format!("<{}", tag);
254 let close = format!("</{}>", tag);
255
256 let start_idx = source.find(&open)?;
257 let after_open = &source[start_idx..];
258 let tag_end = after_open.find('>')?;
260 let content_start = start_idx + tag_end + 1;
261
262 let end_idx = source.rfind(&close)?;
264 if end_idx <= content_start {
265 return None;
266 }
267
268 Some(source[content_start..end_idx].trim().to_string())
269}
270
271fn extract_script_setup(source: &str) -> Option<String> {
272 let marker = "<script setup";
274 let close = "</script>";
275
276 let start_idx = source.find(marker)?;
277 let after_open = &source[start_idx..];
278 let tag_end = after_open.find('>')?;
279 let content_start = start_idx + tag_end + 1;
280
281 let remaining = &source[content_start..];
283 let end_offset = remaining.find(close)?;
284 let end_idx = content_start + end_offset;
285
286 Some(source[content_start..end_idx].trim().to_string())
287}
288
289fn extract_script_server(source: &str) -> Option<String> {
290 let marker = "<script lang=\"java\">";
292 let close = "</script>";
293
294 let start_idx = source.find(marker)?;
295 let content_start = start_idx + marker.len();
296
297 let remaining = &source[content_start..];
299 let end_offset = remaining.find(close)?;
300 let end_idx = content_start + end_offset;
301
302 Some(source[content_start..end_idx].trim().to_string())
303}
304
305fn extract_style(source: &str) -> (Option<String>, bool) {
306 let open = "<style";
307 let close = "</style>";
308
309 let Some(start_idx) = source.find(open) else {
310 return (None, false);
311 };
312 let after_open = &source[start_idx..];
313 let Some(tag_end) = after_open.find('>') else {
314 return (None, false);
315 };
316
317 let tag_attrs = &after_open[..tag_end];
319 let is_scoped = tag_attrs.contains("scoped");
320
321 let content_start = start_idx + tag_end + 1;
322 let remaining = &source[content_start..];
323 let Some(end_offset) = remaining.find(close) else {
324 return (None, false);
325 };
326 let end_idx = content_start + end_offset;
327
328 (Some(source[content_start..end_idx].trim().to_string()), is_scoped)
329}
330
331pub fn scope_id(content: &str) -> String {
336 let mut hasher = DefaultHasher::new();
337 content.hash(&mut hasher);
338 format!("{:08x}", hasher.finish() as u32)
339}
340
341const SKIP_SCOPE_TAGS: &[&str] = &[
345 "slot", "template",
346 "html", "head", "body", "meta", "link", "title",
347 "script", "style", "base", "noscript",
348];
349
350pub fn add_scope_class(html: &str, id: &str) -> String {
355 let mut result = String::with_capacity(html.len() + id.len() * 10);
356 let mut rest = html;
357
358 while let Some(lt_pos) = rest.find('<') {
359 result.push_str(&rest[..lt_pos]);
360 rest = &rest[lt_pos..];
361
362 if rest.starts_with("</") || rest.starts_with("<!--") || rest.starts_with("<!") {
364 if let Some(gt) = rest.find('>') {
365 result.push_str(&rest[..=gt]);
366 rest = &rest[gt + 1..];
367 } else {
368 result.push_str(rest);
369 return result;
370 }
371 continue;
372 }
373
374 let Some(gt) = rest.find('>') else {
376 result.push_str(rest);
377 return result;
378 };
379
380 let tag_name_end = rest[1..]
382 .find(|c: char| !c.is_alphanumeric() && c != '-')
383 .map(|p| p + 1)
384 .unwrap_or(gt);
385 let tag_name = &rest[1..tag_name_end];
386
387 let should_skip = SKIP_SCOPE_TAGS.iter().any(|&t| t.eq_ignore_ascii_case(tag_name));
388
389 if should_skip {
390 result.push_str(&rest[..=gt]);
391 rest = &rest[gt + 1..];
392 continue;
393 }
394
395 let tag = &rest[..gt];
396 let is_self_closing = tag.trim_end().ends_with('/');
397
398 if let Some(class_idx) = tag.find("class=\"") {
399 let after_quote = class_idx + 7;
400 if let Some(end_quote) = tag[after_quote..].find('"') {
401 let insert = after_quote + end_quote;
402 result.push_str(&rest[..insert]);
403 result.push(' ');
404 result.push_str(id);
405 result.push_str(&rest[insert..=gt]);
406 } else {
407 result.push_str(&rest[..=gt]);
408 }
409 } else if is_self_closing {
410 let slash = tag.rfind('/').unwrap();
411 result.push_str(&rest[..slash]);
412 result.push_str("class=\"");
413 result.push_str(id);
414 result.push_str("\" ");
415 result.push_str(&rest[slash..=gt]);
416 } else {
417 result.push_str(&rest[..gt]);
418 result.push_str(" class=\"");
419 result.push_str(id);
420 result.push_str("\">");
421 }
422
423 rest = &rest[gt + 1..];
424 }
425
426 result.push_str(rest);
427 result
428}
429
430pub fn scope_css(css: &str, id: &str) -> String {
436 let suffix = format!(".{id}");
437 let rule_re = Regex::new(r"([^{}]+)\{([^{}]*)\}").unwrap();
438
439 rule_re.replace_all(css, |caps: ®ex::Captures| {
440 let selectors = caps[1].trim();
441 let body = &caps[2];
442
443 let scoped: Vec<String> = selectors
444 .split(',')
445 .map(|s| insert_scope_suffix(s.trim(), &suffix))
446 .collect();
447
448 format!("{} {{{}}}", scoped.join(", "), body)
449 }).to_string()
450}
451
452fn insert_scope_suffix(selector: &str, suffix: &str) -> String {
459 let last_start = selector
461 .rfind(|c: char| c == ' ' || c == '>' || c == '+' || c == '~')
462 .map(|p| p + 1)
463 .unwrap_or(0);
464
465 let last_part = &selector[last_start..];
466
467 if let Some(colon_pos) = last_part.find(':') {
469 let insert_at = last_start + colon_pos;
470 format!("{}{}{}", &selector[..insert_at], suffix, &selector[insert_at..])
471 } else {
472 format!("{}{}", selector, suffix)
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn test_parse_blocks_basic() {
482 let source = r#"
483<script setup lang="ts">
484import Hello from './hello.van'
485</script>
486
487<template>
488 <div>Hello {{ name }}</div>
489</template>
490
491<style scoped>
492.hello { color: red; }
493</style>
494"#;
495 let blocks = parse_blocks(source);
496 assert!(blocks.template.is_some());
497 assert!(blocks.template.unwrap().contains("Hello {{ name }}"));
498 assert!(blocks.script_setup.is_some());
499 assert!(blocks.script_setup.unwrap().contains("import Hello"));
500 assert!(blocks.style.is_some());
501 assert!(blocks.style.unwrap().contains("color: red"));
502 assert!(blocks.script_server.is_none());
503 }
504
505 #[test]
506 fn test_parse_blocks_with_java_script() {
507 let source = r#"
508<template>
509 <div></div>
510</template>
511
512<script setup lang="ts">
513// ts code
514</script>
515
516<script lang="java">
517// java code
518</script>
519"#;
520 let blocks = parse_blocks(source);
521 assert!(blocks.template.is_some());
522 assert!(blocks.script_setup.is_some());
523 assert!(blocks.script_server.is_some());
524 assert!(blocks.script_server.unwrap().contains("java code"));
525 }
526
527 #[test]
528 fn test_parse_blocks_empty() {
529 let blocks = parse_blocks("");
530 assert!(blocks.template.is_none());
531 assert!(blocks.script_setup.is_none());
532 assert!(blocks.script_server.is_none());
533 assert!(blocks.style.is_none());
534 }
535
536 #[test]
537 fn test_parse_blocks_nested_template_slots() {
538 let source = r#"
539<template>
540 <default-layout>
541 <template #title>{{ title }}</template>
542 <h1>Welcome</h1>
543 </default-layout>
544</template>
545
546<script setup lang="ts">
547import DefaultLayout from '../layouts/default.van'
548</script>
549"#;
550 let blocks = parse_blocks(source);
551 let template = blocks.template.unwrap();
552 assert!(template.contains("<default-layout>"), "Should contain opening tag");
553 assert!(template.contains("</default-layout>"), "Should contain closing tag");
554 assert!(template.contains("<template #title>"), "Should contain slot template");
555 assert!(template.contains("<h1>Welcome</h1>"), "Should contain h1");
556 }
557
558 #[test]
559 fn test_parse_imports() {
560 let script = r#"
561import DefaultLayout from '../layouts/default.van'
562import Hello from '../components/hello.van'
563
564defineProps({
565 title: String
566})
567"#;
568 let imports = parse_imports(script);
569 assert_eq!(imports.len(), 2);
570 assert_eq!(imports[0].name, "DefaultLayout");
571 assert_eq!(imports[0].tag_name, "default-layout");
572 assert_eq!(imports[0].path, "../layouts/default.van");
573 assert_eq!(imports[1].name, "Hello");
574 assert_eq!(imports[1].tag_name, "hello");
575 assert_eq!(imports[1].path, "../components/hello.van");
576 }
577
578 #[test]
579 fn test_parse_imports_double_quotes() {
580 let script = r#"import Foo from "../components/foo.van""#;
581 let imports = parse_imports(script);
582 assert_eq!(imports.len(), 1);
583 assert_eq!(imports[0].name, "Foo");
584 assert_eq!(imports[0].path, "../components/foo.van");
585 }
586
587 #[test]
588 fn test_parse_imports_no_van_files() {
589 let script = r#"import { ref } from 'vue'"#;
590 let imports = parse_imports(script);
591 assert!(imports.is_empty());
592 }
593
594 #[test]
595 fn test_pascal_to_kebab() {
596 assert_eq!(pascal_to_kebab("DefaultLayout"), "default-layout");
597 assert_eq!(pascal_to_kebab("Hello"), "hello");
598 assert_eq!(pascal_to_kebab("MyComponent"), "my-component");
599 assert_eq!(pascal_to_kebab("A"), "a");
600 }
601
602 #[test]
605 fn test_style_scoped_detection() {
606 let scoped_source = r#"
607<template><div>Hi</div></template>
608<style scoped>
609.card { color: red; }
610</style>
611"#;
612 let blocks = parse_blocks(scoped_source);
613 assert!(blocks.style_scoped);
614 assert!(blocks.style.unwrap().contains("color: red"));
615
616 let unscoped_source = r#"
617<template><div>Hi</div></template>
618<style>
619.card { color: blue; }
620</style>
621"#;
622 let blocks = parse_blocks(unscoped_source);
623 assert!(!blocks.style_scoped);
624 assert!(blocks.style.unwrap().contains("color: blue"));
625 }
626
627 #[test]
628 fn test_style_scoped_with_lang() {
629 let source = r#"
630<template><div>Hi</div></template>
631<style scoped lang="css">
632h1 { font-size: 2rem; }
633</style>
634"#;
635 let blocks = parse_blocks(source);
636 assert!(blocks.style_scoped);
637 }
638
639 #[test]
640 fn test_scope_id_deterministic() {
641 let id1 = scope_id(".card { color: red; }");
642 let id2 = scope_id(".card { color: red; }");
643 assert_eq!(id1, id2);
644 assert_eq!(id1.len(), 8);
645 let id3 = scope_id("h1 { color: blue; }");
647 assert_ne!(id1, id3);
648 }
649
650 #[test]
651 fn test_add_scope_class_all_elements() {
652 let html = r#"<div class="card"><h1>Title</h1><p>Text</p></div>"#;
653 let result = add_scope_class(html, "a1b2c3d4");
654 assert_eq!(
655 result,
656 r#"<div class="card a1b2c3d4"><h1 class="a1b2c3d4">Title</h1><p class="a1b2c3d4">Text</p></div>"#
657 );
658 }
659
660 #[test]
661 fn test_add_scope_class_no_class() {
662 let html = r#"<div><h1>Title</h1></div>"#;
663 let result = add_scope_class(html, "a1b2c3d4");
664 assert_eq!(result, r#"<div class="a1b2c3d4"><h1 class="a1b2c3d4">Title</h1></div>"#);
665 }
666
667 #[test]
668 fn test_add_scope_class_self_closing() {
669 let html = r#"<div><img src="x.png" /><br /></div>"#;
670 let result = add_scope_class(html, "a1b2c3d4");
671 assert_eq!(
672 result,
673 r#"<div class="a1b2c3d4"><img src="x.png" class="a1b2c3d4" /><br class="a1b2c3d4" /></div>"#
674 );
675 }
676
677 #[test]
678 fn test_add_scope_class_skips_comments() {
679 let html = r#"<!-- comment --><div>Hi</div>"#;
680 let result = add_scope_class(html, "a1b2c3d4");
681 assert_eq!(result, r#"<!-- comment --><div class="a1b2c3d4">Hi</div>"#);
682 }
683
684 #[test]
685 fn test_add_scope_class_skips_slot() {
686 let html = r#"<div><slot /><slot name="x">fallback</slot></div>"#;
687 let result = add_scope_class(html, "a1b2c3d4");
688 assert_eq!(result, r#"<div class="a1b2c3d4"><slot /><slot name="x">fallback</slot></div>"#);
689 }
690
691 #[test]
692 fn test_add_scope_class_skips_structural() {
693 let html = r#"<html><head><meta charset="UTF-8" /></head><body><nav class="x">Hi</nav></body></html>"#;
694 let result = add_scope_class(html, "a1b2c3d4");
695 assert_eq!(
696 result,
697 r#"<html><head><meta charset="UTF-8" /></head><body><nav class="x a1b2c3d4">Hi</nav></body></html>"#
698 );
699 }
700
701 #[test]
702 fn test_scope_css_single_selector() {
703 let css = ".card { border: 1px solid; }";
704 let result = scope_css(css, "a1b2c3d4");
705 assert_eq!(result, ".card.a1b2c3d4 { border: 1px solid; }");
706 }
707
708 #[test]
709 fn test_scope_css_multiple_rules() {
710 let css = ".card { border: 1px solid; }\nh1 { color: navy; }";
711 let result = scope_css(css, "a1b2c3d4");
712 assert!(result.contains(".card.a1b2c3d4 { border: 1px solid; }"));
713 assert!(result.contains("h1.a1b2c3d4 { color: navy; }"));
714 }
715
716 #[test]
717 fn test_scope_css_comma_selectors() {
718 let css = ".card, .box { border: 1px solid; }";
719 let result = scope_css(css, "a1b2c3d4");
720 assert_eq!(result, ".card.a1b2c3d4, .box.a1b2c3d4 { border: 1px solid; }");
721 }
722
723 #[test]
724 fn test_scope_css_descendant_selector() {
725 let css = ".card h1 { color: navy; }";
726 let result = scope_css(css, "a1b2c3d4");
727 assert_eq!(result, ".card h1.a1b2c3d4 { color: navy; }");
728 }
729
730 #[test]
731 fn test_scope_css_pseudo_class() {
732 let css = ".demo-list a:hover { text-decoration: underline; }";
733 let result = scope_css(css, "a1b2c3d4");
734 assert_eq!(result, ".demo-list a.a1b2c3d4:hover { text-decoration: underline; }");
735 }
736
737 #[test]
738 fn test_scope_css_pseudo_element() {
739 let css = ".item::before { content: '-'; }";
740 let result = scope_css(css, "a1b2c3d4");
741 assert_eq!(result, ".item.a1b2c3d4::before { content: '-'; }");
742 }
743
744 #[test]
745 fn test_scope_css_no_pseudo() {
746 let css = "h1 { font-size: 2rem; }";
747 let result = scope_css(css, "a1b2c3d4");
748 assert_eq!(result, "h1.a1b2c3d4 { font-size: 2rem; }");
749 }
750
751 #[test]
754 fn test_parse_define_props_simple() {
755 let script = "defineProps({ title: String, count: Number })";
756 let props = parse_define_props(script);
757 assert_eq!(props.len(), 2);
758 assert_eq!(props[0].name, "title");
759 assert_eq!(props[0].prop_type, Some("String".to_string()));
760 assert!(!props[0].required);
761 assert_eq!(props[1].name, "count");
762 assert_eq!(props[1].prop_type, Some("Number".to_string()));
763 assert!(!props[1].required);
764 }
765
766 #[test]
767 fn test_parse_define_props_with_required() {
768 let script = "defineProps({ user: { type: Object, required: true } })";
769 let props = parse_define_props(script);
770 assert_eq!(props.len(), 1);
771 assert_eq!(props[0].name, "user");
772 assert_eq!(props[0].prop_type, Some("Object".to_string()));
773 assert!(props[0].required);
774 }
775
776 #[test]
777 fn test_parse_define_props_mixed() {
778 let script = r#"defineProps({
779 title: String,
780 user: { type: Object, required: true },
781 count: Number
782})"#;
783 let props = parse_define_props(script);
784 assert_eq!(props.len(), 3);
785 assert_eq!(props[0].name, "title");
786 assert_eq!(props[0].prop_type, Some("String".to_string()));
787 assert!(!props[0].required);
788 assert_eq!(props[1].name, "user");
789 assert_eq!(props[1].prop_type, Some("Object".to_string()));
790 assert!(props[1].required);
791 assert_eq!(props[2].name, "count");
792 assert_eq!(props[2].prop_type, Some("Number".to_string()));
793 assert!(!props[2].required);
794 }
795
796 #[test]
797 fn test_parse_define_props_missing() {
798 let script = "const count = ref(0)";
799 let props = parse_define_props(script);
800 assert!(props.is_empty());
801 }
802
803 #[test]
804 fn test_parse_define_props_empty() {
805 let script = "defineProps({})";
806 let props = parse_define_props(script);
807 assert!(props.is_empty());
808 }
809
810 #[test]
811 fn test_parse_blocks_includes_props() {
812 let source = r#"
813<script setup lang="ts">
814defineProps({ title: String, count: Number })
815</script>
816
817<template>
818 <h1>{{ title }}</h1>
819</template>
820"#;
821 let blocks = parse_blocks(source);
822 assert_eq!(blocks.props.len(), 2);
823 assert_eq!(blocks.props[0].name, "title");
824 assert_eq!(blocks.props[1].name, "count");
825 }
826
827 #[test]
830 fn test_parse_script_imports_ts() {
831 let script = r#"
832import { formatDate } from '../utils/format.ts'
833import DefaultLayout from '../layouts/default.van'
834const count = ref(0)
835"#;
836 let imports = parse_script_imports(script);
837 assert_eq!(imports.len(), 1);
838 assert_eq!(imports[0].path, "../utils/format.ts");
839 assert!(!imports[0].is_type_only);
840 assert!(imports[0].raw.contains("formatDate"));
841 }
842
843 #[test]
844 fn test_parse_script_imports_type_only() {
845 let script = r#"
846import type { User } from '../types/models.ts'
847import { formatDate } from '../utils/format.ts'
848"#;
849 let imports = parse_script_imports(script);
850 assert_eq!(imports.len(), 2);
851 assert!(imports[0].is_type_only);
852 assert_eq!(imports[0].path, "../types/models.ts");
853 assert!(!imports[1].is_type_only);
854 assert_eq!(imports[1].path, "../utils/format.ts");
855 }
856
857 #[test]
858 fn test_parse_script_imports_js() {
859 let script = r#"import foo from '../utils/helper.js'"#;
860 let imports = parse_script_imports(script);
861 assert_eq!(imports.len(), 1);
862 assert_eq!(imports[0].path, "../utils/helper.js");
863 assert!(!imports[0].is_type_only);
864 }
865
866 #[test]
867 fn test_parse_script_imports_ignores_van() {
868 let script = r#"
869import Hello from './hello.van'
870import Foo from '../foo.van'
871"#;
872 let imports = parse_script_imports(script);
873 assert!(imports.is_empty());
874 }
875
876 #[test]
877 fn test_parse_script_imports_ignores_bare() {
878 let script = r#"import { ref } from 'vue'"#;
879 let imports = parse_script_imports(script);
880 assert!(imports.is_empty());
881 }
882
883 #[test]
884 fn test_parse_script_imports_mixed_type() {
885 let script = r#"import { type User, formatDate } from '../utils.ts'"#;
887 let imports = parse_script_imports(script);
888 assert_eq!(imports.len(), 1);
889 assert!(!imports[0].is_type_only);
890 }
891
892 #[test]
893 fn test_parse_imports_scoped_package() {
894 let script = r#"
895import VanButton from '@van-ui/button/button.van'
896import DefaultLayout from '../layouts/default.van'
897"#;
898 let imports = parse_imports(script);
899 assert_eq!(imports.len(), 2);
900 assert_eq!(imports[0].name, "VanButton");
901 assert_eq!(imports[0].tag_name, "van-button");
902 assert_eq!(imports[0].path, "@van-ui/button/button.van");
903 assert_eq!(imports[1].name, "DefaultLayout");
904 assert_eq!(imports[1].path, "../layouts/default.van");
905 }
906
907 #[test]
908 fn test_parse_script_imports_scoped_package() {
909 let script = r#"
910import { formatDate } from '@van-ui/utils/format.ts'
911import { helper } from '../utils/helper.ts'
912"#;
913 let imports = parse_script_imports(script);
914 assert_eq!(imports.len(), 2);
915 assert_eq!(imports[0].path, "@van-ui/utils/format.ts");
916 assert_eq!(imports[1].path, "../utils/helper.ts");
917 }
918
919 #[test]
920 fn test_parse_script_imports_tsx_jsx() {
921 let script = r#"
922import { render } from '../lib/render.tsx'
923import { helper } from '../lib/helper.jsx'
924"#;
925 let imports = parse_script_imports(script);
926 assert_eq!(imports.len(), 2);
927 assert_eq!(imports[0].path, "../lib/render.tsx");
928 assert_eq!(imports[1].path, "../lib/helper.jsx");
929 }
930}