1use std::borrow::Cow;
9
10use fallow_extract::sfc::SfcScript;
11use fallow_mutation::{
12 Instrumented, MutationError, MutationOptions, ScriptBlock, ScriptRole, SourceKind,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub(super) enum Component {
18 Vue,
19 Svelte,
20 Astro,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(super) enum Layout {
26 Script(SourceKind),
28 SvelteModule(SourceKind),
31 Component(Component),
33}
34
35impl Layout {
36 pub(super) fn from_path(path: &str) -> Option<Self> {
38 if let Some(kind) = SourceKind::from_path(path) {
39 let stem = path.rsplit_once('.').map_or(path, |(stem, _)| stem);
40 let runes =
41 stem.ends_with(".svelte") && matches!(kind, SourceKind::Js | SourceKind::Ts);
42 return Some(if runes {
43 Self::SvelteModule(kind)
44 } else {
45 Self::Script(kind)
46 });
47 }
48 let component = match path.rsplit_once('.')?.1 {
49 "vue" => Component::Vue,
50 "svelte" => Component::Svelte,
51 "astro" => Component::Astro,
52 _ => return None,
53 };
54 Some(Self::Component(component))
55 }
56
57 pub(super) fn instrument(
59 self,
60 source: &str,
61 path: &str,
62 options: &MutationOptions,
63 ) -> Result<Instrumented, MutationError> {
64 match self {
65 Self::Script(kind) => fallow_mutation::instrument(source, path, kind, options),
66 Self::SvelteModule(kind) => {
67 let block = ScriptBlock {
68 span: (
69 0,
70 u32::try_from(source.len()).map_err(|_| MutationError::TooLarge)?,
71 ),
72 kind,
73 role: ScriptRole::SvelteModule,
74 };
75 fallow_mutation::instrument_blocks(source, path, &[block], options)
76 }
77 Self::Component(component) => fallow_mutation::instrument_blocks(
78 source,
79 path,
80 &script_blocks(component, source),
81 options,
82 ),
83 }
84 }
85
86 pub(super) fn parse_view(self, source: &str) -> (Cow<'_, str>, SourceKind) {
91 let component = match self {
92 Self::Script(kind) | Self::SvelteModule(kind) => return (Cow::Borrowed(source), kind),
93 Self::Component(component) => component,
94 };
95 let blocks = script_blocks(component, source);
96 let mut masked: Vec<u8> = source
97 .bytes()
98 .map(|byte| if byte == b'\n' { b'\n' } else { b' ' })
99 .collect();
100 for block in &blocks {
101 let (start, end) = (block.span.0 as usize, block.span.1 as usize);
102 masked[start..end].copy_from_slice(&source.as_bytes()[start..end]);
103 }
104 let typescript = blocks
105 .iter()
106 .any(|block| matches!(block.kind, SourceKind::Ts | SourceKind::Tsx));
107 let jsx = blocks
108 .iter()
109 .any(|block| matches!(block.kind, SourceKind::Jsx | SourceKind::Tsx));
110 let kind = match (typescript, jsx) {
111 (true, true) => SourceKind::Tsx,
112 (true, false) => SourceKind::Ts,
113 (false, true) => SourceKind::Jsx,
114 (false, false) => SourceKind::Js,
115 };
116 let text = String::from_utf8(masked).unwrap_or_default();
119 (Cow::Owned(text), kind)
120 }
121
122 pub(super) fn script_spans(self, source: &str) -> Option<Vec<(u32, u32)>> {
125 match self {
126 Self::Script(_) | Self::SvelteModule(_) => None,
127 Self::Component(component) => Some(
128 script_blocks(component, source)
129 .iter()
130 .map(|block| block.span)
131 .collect(),
132 ),
133 }
134 }
135}
136
137fn script_blocks(component: Component, source: &str) -> Vec<ScriptBlock> {
140 match component {
141 Component::Vue | Component::Svelte => fallow_extract::sfc::extract_sfc_scripts(source)
142 .iter()
143 .filter(|script| script.src.is_none())
144 .filter_map(|script| {
145 let role = match (component, script.is_setup, script.is_context_module) {
146 (Component::Vue, true, _) => ScriptRole::VueSetup,
147 (Component::Vue, false, _) => ScriptRole::VueScript,
148 (_, _, true) => ScriptRole::SvelteModule,
149 (_, _, false) => ScriptRole::SvelteInstance,
150 };
151 block(script, role)
152 })
153 .collect(),
154 Component::Astro => fallow_extract::astro::extract_astro_frontmatter(source)
155 .iter()
156 .filter_map(|script| block(script, ScriptRole::AstroFrontmatter))
157 .collect(),
158 }
159}
160
161fn block(script: &SfcScript, role: ScriptRole) -> Option<ScriptBlock> {
162 let start = u32::try_from(script.byte_offset).ok()?;
163 let end = u32::try_from(script.byte_offset + script.body.len()).ok()?;
164 let kind = match (script.is_typescript, script.is_jsx) {
165 (true, true) => SourceKind::Tsx,
166 (true, false) => SourceKind::Ts,
167 (false, true) => SourceKind::Jsx,
168 (false, false) => SourceKind::Js,
169 };
170 Some(ScriptBlock {
171 span: (start, end),
172 kind,
173 role,
174 })
175}
176
177#[must_use]
181pub fn is_component_file(path: &str) -> bool {
182 matches!(Layout::from_path(path), Some(Layout::Component(_)))
183}
184
185#[must_use]
192pub fn instrument_source(
193 source: &str,
194 path: &str,
195 options: &MutationOptions,
196) -> Option<Result<Instrumented, MutationError>> {
197 Layout::from_path(path).map(|layout| layout.instrument(source, path, options))
198}