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 search_start = source.rfind("</template>")
311 .map(|i| i + "</template>".len())
312 .unwrap_or(0);
313
314 let Some(rel_idx) = source[search_start..].find(open) else {
315 return (None, false);
316 };
317 let start_idx = search_start + rel_idx;
318 let after_open = &source[start_idx..];
319 let Some(tag_end) = after_open.find('>') else {
320 return (None, false);
321 };
322
323 let tag_attrs = &after_open[..tag_end];
325 let is_scoped = tag_attrs.contains("scoped");
326
327 let content_start = start_idx + tag_end + 1;
328 let remaining = &source[content_start..];
329 let Some(end_offset) = remaining.find(close) else {
330 return (None, false);
331 };
332 let end_idx = content_start + end_offset;
333
334 (Some(source[content_start..end_idx].trim().to_string()), is_scoped)
335}
336
337pub fn scope_id(content: &str) -> String {
342 let mut hasher = DefaultHasher::new();
343 content.hash(&mut hasher);
344 format!("{:08x}", hasher.finish() as u32)
345}
346
347const SKIP_SCOPE_TAGS: &[&str] = &[
351 "slot", "template",
352 "html", "head", "body", "meta", "link", "title",
353 "script", "style", "base", "noscript",
354];
355
356pub fn add_scope_class(html: &str, id: &str) -> String {
361 let mut result = String::with_capacity(html.len() + id.len() * 10);
362 let mut rest = html;
363
364 while let Some(lt_pos) = rest.find('<') {
365 result.push_str(&rest[..lt_pos]);
366 rest = &rest[lt_pos..];
367
368 if rest.starts_with("</") || rest.starts_with("<!--") || rest.starts_with("<!") {
370 if let Some(gt) = rest.find('>') {
371 result.push_str(&rest[..=gt]);
372 rest = &rest[gt + 1..];
373 } else {
374 result.push_str(rest);
375 return result;
376 }
377 continue;
378 }
379
380 let Some(gt) = rest.find('>') else {
382 result.push_str(rest);
383 return result;
384 };
385
386 let tag_name_end = rest[1..]
388 .find(|c: char| !c.is_alphanumeric() && c != '-')
389 .map(|p| p + 1)
390 .unwrap_or(gt);
391 let tag_name = &rest[1..tag_name_end];
392
393 let should_skip = SKIP_SCOPE_TAGS.iter().any(|&t| t.eq_ignore_ascii_case(tag_name));
394
395 if should_skip {
396 result.push_str(&rest[..=gt]);
397 rest = &rest[gt + 1..];
398 continue;
399 }
400
401 let tag = &rest[..gt];
402 let is_self_closing = tag.trim_end().ends_with('/');
403
404 if let Some(class_idx) = tag.find("class=\"") {
405 let after_quote = class_idx + 7;
406 if let Some(end_quote) = tag[after_quote..].find('"') {
407 let insert = after_quote + end_quote;
408 result.push_str(&rest[..insert]);
409 result.push(' ');
410 result.push_str(id);
411 result.push_str(&rest[insert..=gt]);
412 } else {
413 result.push_str(&rest[..=gt]);
414 }
415 } else if is_self_closing {
416 let slash = tag.rfind('/').unwrap();
417 result.push_str(&rest[..slash]);
418 result.push_str("class=\"");
419 result.push_str(id);
420 result.push_str("\" ");
421 result.push_str(&rest[slash..=gt]);
422 } else {
423 result.push_str(&rest[..gt]);
424 result.push_str(" class=\"");
425 result.push_str(id);
426 result.push_str("\">");
427 }
428
429 rest = &rest[gt + 1..];
430 }
431
432 result.push_str(rest);
433 result
434}
435
436pub fn scope_css(css: &str, id: &str) -> String {
442 let suffix = format!(".{id}");
443 let rule_re = Regex::new(r"([^{}]+)\{([^{}]*)\}").unwrap();
444
445 rule_re.replace_all(css, |caps: ®ex::Captures| {
446 let selectors = caps[1].trim();
447 let body = &caps[2];
448
449 let scoped: Vec<String> = selectors
450 .split(',')
451 .map(|s| insert_scope_suffix(s.trim(), &suffix))
452 .collect();
453
454 format!("{} {{{}}}", scoped.join(", "), body)
455 }).to_string()
456}
457
458fn insert_scope_suffix(selector: &str, suffix: &str) -> String {
465 let last_start = selector
467 .rfind(|c: char| c == ' ' || c == '>' || c == '+' || c == '~')
468 .map(|p| p + 1)
469 .unwrap_or(0);
470
471 let last_part = &selector[last_start..];
472
473 if let Some(colon_pos) = last_part.find(':') {
475 let insert_at = last_start + colon_pos;
476 format!("{}{}{}", &selector[..insert_at], suffix, &selector[insert_at..])
477 } else {
478 format!("{}{}", selector, suffix)
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn test_parse_blocks_basic() {
488 let source = r#"
489<script setup lang="ts">
490import Hello from './hello.van'
491</script>
492
493<template>
494 <div>Hello {{ name }}</div>
495</template>
496
497<style scoped>
498.hello { color: red; }
499</style>
500"#;
501 let blocks = parse_blocks(source);
502 assert!(blocks.template.is_some());
503 assert!(blocks.template.unwrap().contains("Hello {{ name }}"));
504 assert!(blocks.script_setup.is_some());
505 assert!(blocks.script_setup.unwrap().contains("import Hello"));
506 assert!(blocks.style.is_some());
507 assert!(blocks.style.unwrap().contains("color: red"));
508 assert!(blocks.script_server.is_none());
509 }
510
511 #[test]
512 fn test_parse_blocks_with_java_script() {
513 let source = r#"
514<template>
515 <div></div>
516</template>
517
518<script setup lang="ts">
519// ts code
520</script>
521
522<script lang="java">
523// java code
524</script>
525"#;
526 let blocks = parse_blocks(source);
527 assert!(blocks.template.is_some());
528 assert!(blocks.script_setup.is_some());
529 assert!(blocks.script_server.is_some());
530 assert!(blocks.script_server.unwrap().contains("java code"));
531 }
532
533 #[test]
534 fn test_parse_blocks_empty() {
535 let blocks = parse_blocks("");
536 assert!(blocks.template.is_none());
537 assert!(blocks.script_setup.is_none());
538 assert!(blocks.script_server.is_none());
539 assert!(blocks.style.is_none());
540 }
541
542 #[test]
543 fn test_parse_blocks_nested_template_slots() {
544 let source = r#"
545<template>
546 <default-layout>
547 <template #title>{{ title }}</template>
548 <h1>Welcome</h1>
549 </default-layout>
550</template>
551
552<script setup lang="ts">
553import DefaultLayout from '../layouts/default.van'
554</script>
555"#;
556 let blocks = parse_blocks(source);
557 let template = blocks.template.unwrap();
558 assert!(template.contains("<default-layout>"), "Should contain opening tag");
559 assert!(template.contains("</default-layout>"), "Should contain closing tag");
560 assert!(template.contains("<template #title>"), "Should contain slot template");
561 assert!(template.contains("<h1>Welcome</h1>"), "Should contain h1");
562 }
563
564 #[test]
565 fn test_parse_imports() {
566 let script = r#"
567import DefaultLayout from '../layouts/default.van'
568import Hello from '../components/hello.van'
569
570defineProps({
571 title: String
572})
573"#;
574 let imports = parse_imports(script);
575 assert_eq!(imports.len(), 2);
576 assert_eq!(imports[0].name, "DefaultLayout");
577 assert_eq!(imports[0].tag_name, "default-layout");
578 assert_eq!(imports[0].path, "../layouts/default.van");
579 assert_eq!(imports[1].name, "Hello");
580 assert_eq!(imports[1].tag_name, "hello");
581 assert_eq!(imports[1].path, "../components/hello.van");
582 }
583
584 #[test]
585 fn test_parse_imports_double_quotes() {
586 let script = r#"import Foo from "../components/foo.van""#;
587 let imports = parse_imports(script);
588 assert_eq!(imports.len(), 1);
589 assert_eq!(imports[0].name, "Foo");
590 assert_eq!(imports[0].path, "../components/foo.van");
591 }
592
593 #[test]
594 fn test_parse_imports_no_van_files() {
595 let script = r#"import { ref } from 'vue'"#;
596 let imports = parse_imports(script);
597 assert!(imports.is_empty());
598 }
599
600 #[test]
601 fn test_pascal_to_kebab() {
602 assert_eq!(pascal_to_kebab("DefaultLayout"), "default-layout");
603 assert_eq!(pascal_to_kebab("Hello"), "hello");
604 assert_eq!(pascal_to_kebab("MyComponent"), "my-component");
605 assert_eq!(pascal_to_kebab("A"), "a");
606 }
607
608 #[test]
611 fn test_style_scoped_detection() {
612 let scoped_source = r#"
613<template><div>Hi</div></template>
614<style scoped>
615.card { color: red; }
616</style>
617"#;
618 let blocks = parse_blocks(scoped_source);
619 assert!(blocks.style_scoped);
620 assert!(blocks.style.unwrap().contains("color: red"));
621
622 let unscoped_source = r#"
623<template><div>Hi</div></template>
624<style>
625.card { color: blue; }
626</style>
627"#;
628 let blocks = parse_blocks(unscoped_source);
629 assert!(!blocks.style_scoped);
630 assert!(blocks.style.unwrap().contains("color: blue"));
631 }
632
633 #[test]
634 fn test_style_scoped_with_lang() {
635 let source = r#"
636<template><div>Hi</div></template>
637<style scoped lang="css">
638h1 { font-size: 2rem; }
639</style>
640"#;
641 let blocks = parse_blocks(source);
642 assert!(blocks.style_scoped);
643 }
644
645 #[test]
646 fn test_scope_id_deterministic() {
647 let id1 = scope_id(".card { color: red; }");
648 let id2 = scope_id(".card { color: red; }");
649 assert_eq!(id1, id2);
650 assert_eq!(id1.len(), 8);
651 let id3 = scope_id("h1 { color: blue; }");
653 assert_ne!(id1, id3);
654 }
655
656 #[test]
657 fn test_add_scope_class_all_elements() {
658 let html = r#"<div class="card"><h1>Title</h1><p>Text</p></div>"#;
659 let result = add_scope_class(html, "a1b2c3d4");
660 assert_eq!(
661 result,
662 r#"<div class="card a1b2c3d4"><h1 class="a1b2c3d4">Title</h1><p class="a1b2c3d4">Text</p></div>"#
663 );
664 }
665
666 #[test]
667 fn test_add_scope_class_no_class() {
668 let html = r#"<div><h1>Title</h1></div>"#;
669 let result = add_scope_class(html, "a1b2c3d4");
670 assert_eq!(result, r#"<div class="a1b2c3d4"><h1 class="a1b2c3d4">Title</h1></div>"#);
671 }
672
673 #[test]
674 fn test_add_scope_class_self_closing() {
675 let html = r#"<div><img src="x.png" /><br /></div>"#;
676 let result = add_scope_class(html, "a1b2c3d4");
677 assert_eq!(
678 result,
679 r#"<div class="a1b2c3d4"><img src="x.png" class="a1b2c3d4" /><br class="a1b2c3d4" /></div>"#
680 );
681 }
682
683 #[test]
684 fn test_add_scope_class_skips_comments() {
685 let html = r#"<!-- comment --><div>Hi</div>"#;
686 let result = add_scope_class(html, "a1b2c3d4");
687 assert_eq!(result, r#"<!-- comment --><div class="a1b2c3d4">Hi</div>"#);
688 }
689
690 #[test]
691 fn test_add_scope_class_skips_slot() {
692 let html = r#"<div><slot /><slot name="x">fallback</slot></div>"#;
693 let result = add_scope_class(html, "a1b2c3d4");
694 assert_eq!(result, r#"<div class="a1b2c3d4"><slot /><slot name="x">fallback</slot></div>"#);
695 }
696
697 #[test]
698 fn test_add_scope_class_skips_structural() {
699 let html = r#"<html><head><meta charset="UTF-8" /></head><body><nav class="x">Hi</nav></body></html>"#;
700 let result = add_scope_class(html, "a1b2c3d4");
701 assert_eq!(
702 result,
703 r#"<html><head><meta charset="UTF-8" /></head><body><nav class="x a1b2c3d4">Hi</nav></body></html>"#
704 );
705 }
706
707 #[test]
708 fn test_scope_css_single_selector() {
709 let css = ".card { border: 1px solid; }";
710 let result = scope_css(css, "a1b2c3d4");
711 assert_eq!(result, ".card.a1b2c3d4 { border: 1px solid; }");
712 }
713
714 #[test]
715 fn test_scope_css_multiple_rules() {
716 let css = ".card { border: 1px solid; }\nh1 { color: navy; }";
717 let result = scope_css(css, "a1b2c3d4");
718 assert!(result.contains(".card.a1b2c3d4 { border: 1px solid; }"));
719 assert!(result.contains("h1.a1b2c3d4 { color: navy; }"));
720 }
721
722 #[test]
723 fn test_scope_css_comma_selectors() {
724 let css = ".card, .box { border: 1px solid; }";
725 let result = scope_css(css, "a1b2c3d4");
726 assert_eq!(result, ".card.a1b2c3d4, .box.a1b2c3d4 { border: 1px solid; }");
727 }
728
729 #[test]
730 fn test_scope_css_descendant_selector() {
731 let css = ".card h1 { color: navy; }";
732 let result = scope_css(css, "a1b2c3d4");
733 assert_eq!(result, ".card h1.a1b2c3d4 { color: navy; }");
734 }
735
736 #[test]
737 fn test_scope_css_pseudo_class() {
738 let css = ".demo-list a:hover { text-decoration: underline; }";
739 let result = scope_css(css, "a1b2c3d4");
740 assert_eq!(result, ".demo-list a.a1b2c3d4:hover { text-decoration: underline; }");
741 }
742
743 #[test]
744 fn test_scope_css_pseudo_element() {
745 let css = ".item::before { content: '-'; }";
746 let result = scope_css(css, "a1b2c3d4");
747 assert_eq!(result, ".item.a1b2c3d4::before { content: '-'; }");
748 }
749
750 #[test]
751 fn test_scope_css_no_pseudo() {
752 let css = "h1 { font-size: 2rem; }";
753 let result = scope_css(css, "a1b2c3d4");
754 assert_eq!(result, "h1.a1b2c3d4 { font-size: 2rem; }");
755 }
756
757 #[test]
760 fn test_parse_define_props_simple() {
761 let script = "defineProps({ title: String, count: Number })";
762 let props = parse_define_props(script);
763 assert_eq!(props.len(), 2);
764 assert_eq!(props[0].name, "title");
765 assert_eq!(props[0].prop_type, Some("String".to_string()));
766 assert!(!props[0].required);
767 assert_eq!(props[1].name, "count");
768 assert_eq!(props[1].prop_type, Some("Number".to_string()));
769 assert!(!props[1].required);
770 }
771
772 #[test]
773 fn test_parse_define_props_with_required() {
774 let script = "defineProps({ user: { type: Object, required: true } })";
775 let props = parse_define_props(script);
776 assert_eq!(props.len(), 1);
777 assert_eq!(props[0].name, "user");
778 assert_eq!(props[0].prop_type, Some("Object".to_string()));
779 assert!(props[0].required);
780 }
781
782 #[test]
783 fn test_parse_define_props_mixed() {
784 let script = r#"defineProps({
785 title: String,
786 user: { type: Object, required: true },
787 count: Number
788})"#;
789 let props = parse_define_props(script);
790 assert_eq!(props.len(), 3);
791 assert_eq!(props[0].name, "title");
792 assert_eq!(props[0].prop_type, Some("String".to_string()));
793 assert!(!props[0].required);
794 assert_eq!(props[1].name, "user");
795 assert_eq!(props[1].prop_type, Some("Object".to_string()));
796 assert!(props[1].required);
797 assert_eq!(props[2].name, "count");
798 assert_eq!(props[2].prop_type, Some("Number".to_string()));
799 assert!(!props[2].required);
800 }
801
802 #[test]
803 fn test_parse_define_props_missing() {
804 let script = "const count = ref(0)";
805 let props = parse_define_props(script);
806 assert!(props.is_empty());
807 }
808
809 #[test]
810 fn test_parse_define_props_empty() {
811 let script = "defineProps({})";
812 let props = parse_define_props(script);
813 assert!(props.is_empty());
814 }
815
816 #[test]
817 fn test_parse_blocks_includes_props() {
818 let source = r#"
819<script setup lang="ts">
820defineProps({ title: String, count: Number })
821</script>
822
823<template>
824 <h1>{{ title }}</h1>
825</template>
826"#;
827 let blocks = parse_blocks(source);
828 assert_eq!(blocks.props.len(), 2);
829 assert_eq!(blocks.props[0].name, "title");
830 assert_eq!(blocks.props[1].name, "count");
831 }
832
833 #[test]
836 fn test_parse_script_imports_ts() {
837 let script = r#"
838import { formatDate } from '../utils/format.ts'
839import DefaultLayout from '../layouts/default.van'
840const count = ref(0)
841"#;
842 let imports = parse_script_imports(script);
843 assert_eq!(imports.len(), 1);
844 assert_eq!(imports[0].path, "../utils/format.ts");
845 assert!(!imports[0].is_type_only);
846 assert!(imports[0].raw.contains("formatDate"));
847 }
848
849 #[test]
850 fn test_parse_script_imports_type_only() {
851 let script = r#"
852import type { User } from '../types/models.ts'
853import { formatDate } from '../utils/format.ts'
854"#;
855 let imports = parse_script_imports(script);
856 assert_eq!(imports.len(), 2);
857 assert!(imports[0].is_type_only);
858 assert_eq!(imports[0].path, "../types/models.ts");
859 assert!(!imports[1].is_type_only);
860 assert_eq!(imports[1].path, "../utils/format.ts");
861 }
862
863 #[test]
864 fn test_parse_script_imports_js() {
865 let script = r#"import foo from '../utils/helper.js'"#;
866 let imports = parse_script_imports(script);
867 assert_eq!(imports.len(), 1);
868 assert_eq!(imports[0].path, "../utils/helper.js");
869 assert!(!imports[0].is_type_only);
870 }
871
872 #[test]
873 fn test_parse_script_imports_ignores_van() {
874 let script = r#"
875import Hello from './hello.van'
876import Foo from '../foo.van'
877"#;
878 let imports = parse_script_imports(script);
879 assert!(imports.is_empty());
880 }
881
882 #[test]
883 fn test_parse_script_imports_ignores_bare() {
884 let script = r#"import { ref } from 'vue'"#;
885 let imports = parse_script_imports(script);
886 assert!(imports.is_empty());
887 }
888
889 #[test]
890 fn test_parse_script_imports_mixed_type() {
891 let script = r#"import { type User, formatDate } from '../utils.ts'"#;
893 let imports = parse_script_imports(script);
894 assert_eq!(imports.len(), 1);
895 assert!(!imports[0].is_type_only);
896 }
897
898 #[test]
899 fn test_parse_imports_scoped_package() {
900 let script = r#"
901import VanButton from '@van-ui/button/button.van'
902import DefaultLayout from '../layouts/default.van'
903"#;
904 let imports = parse_imports(script);
905 assert_eq!(imports.len(), 2);
906 assert_eq!(imports[0].name, "VanButton");
907 assert_eq!(imports[0].tag_name, "van-button");
908 assert_eq!(imports[0].path, "@van-ui/button/button.van");
909 assert_eq!(imports[1].name, "DefaultLayout");
910 assert_eq!(imports[1].path, "../layouts/default.van");
911 }
912
913 #[test]
914 fn test_parse_script_imports_scoped_package() {
915 let script = r#"
916import { formatDate } from '@van-ui/utils/format.ts'
917import { helper } from '../utils/helper.ts'
918"#;
919 let imports = parse_script_imports(script);
920 assert_eq!(imports.len(), 2);
921 assert_eq!(imports[0].path, "@van-ui/utils/format.ts");
922 assert_eq!(imports[1].path, "../utils/helper.ts");
923 }
924
925 #[test]
926 fn test_parse_script_imports_tsx_jsx() {
927 let script = r#"
928import { render } from '../lib/render.tsx'
929import { helper } from '../lib/helper.jsx'
930"#;
931 let imports = parse_script_imports(script);
932 assert_eq!(imports.len(), 2);
933 assert_eq!(imports[0].path, "../lib/render.tsx");
934 assert_eq!(imports[1].path, "../lib/helper.jsx");
935 }
936}