Skip to main content

Library/Fn/Worker/
Detect.rs

1//! Worker file detection
2//!
3//! Detects web worker files in the project and classifies them.
4
5use std::path::Path;
6
7use walkdir::WalkDir;
8
9use super::{WorkerConfig, WorkerInfo, WorkerType};
10
11/// Detects worker files in a project
12pub struct WorkerDetector {
13	config:WorkerConfig,
14}
15
16impl WorkerDetector {
17	pub fn new(config:WorkerConfig) -> Self { Self { config } }
18
19	/// Detect all worker files in a directory
20	pub fn detect_workers(&self, root_dir:&Path) -> Vec<WorkerInfo> {
21		let mut workers = Vec::new();
22
23		for entry in WalkDir::new(root_dir).follow_links(true).into_iter().filter_map(|e| e.ok()) {
24			let path = entry.path();
25
26			if self.is_worker_file(path) {
27				if let Some(worker_info) = self.create_worker_info(path) {
28					workers.push(worker_info);
29				}
30			}
31		}
32
33		workers
34	}
35
36	/// Check if a file is a worker file based on naming conventions
37	pub fn is_worker_file(&self, path:&Path) -> bool {
38		if !path.is_file() {
39			return false;
40		}
41
42		let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
43
44		// Check common worker file patterns
45		let worker_patterns = [
46			".worker.ts",
47			".worker.js",
48			".worker.tsx",
49			".worker.jsx",
50			"-worker.ts",
51			"-worker.js",
52			"worker.ts",
53			"worker.js",
54			"SharedWorker.ts",
55			"SharedWorker.js",
56		];
57
58		for pattern in worker_patterns {
59			if file_name.ends_with(pattern) {
60				return true;
61			}
62		}
63
64		// Check for explicit worker markers in the file
65		if let Ok(content) = std::fs::read_to_string(path) {
66			return self.contains_worker_marker(&content);
67		}
68
69		false
70	}
71
72	/// Create worker info from a file path
73	fn create_worker_info(&self, path:&Path) -> Option<WorkerInfo> {
74		let file_name = path.file_name()?.to_str()?;
75
76		let worker_type = if file_name.contains("SharedWorker") || file_name.contains("shared") {
77			// Shared workers are typically classic
78			WorkerType::Classic
79		} else {
80			self.config.worker_type
81		};
82
83		let is_shared = file_name.contains("SharedWorker") || file_name.contains("shared");
84
85		let source_path = path.to_string_lossy().to_string();
86
87		let name = path.file_stem()?.to_str()?.replace(".worker", "").replace("-worker", "");
88
89		let output_path = Path::new(&self.config.output_dir)
90			.join(format!("{}.js", name))
91			.to_string_lossy()
92			.to_string();
93
94		Some(WorkerInfo { source_path, output_path, name, worker_type, dependencies:Vec::new(), is_shared })
95	}
96
97	/// Check if the file contains a worker marker
98	fn contains_worker_marker(&self, content:&str) -> bool {
99		let markers = [
100			"new Worker(",
101			"new SharedWorker(",
102			"self.onmessage",
103			"self.postMessage",
104			"importScripts(",
105			"// @worker",
106			"//worker",
107		];
108
109		for marker in markers {
110			if content.contains(marker) {
111				return true;
112			}
113		}
114
115		false
116	}
117
118	/// Extract dependencies from a worker file
119	pub fn extract_dependencies(&self, path:&Path) -> Vec<String> {
120		let mut deps = Vec::new();
121
122		if let Ok(content) = std::fs::read_to_string(path) {
123			// Extract import statements
124			for line in content.lines() {
125				let trimmed = line.trim();
126
127				// Match import from '...' or import "..."
128				if trimmed.starts_with("import ") {
129					if let Some(from_start) = trimmed.find("from") {
130						let import_part = &trimmed[from_start + 4..];
131
132						if let Some(path_start) = import_part.find('"') {
133							let path_end = import_part[path_start + 1..].find('"');
134
135							if let Some(end) = path_end {
136								let import_path = &import_part[path_start + 1..path_start + 1 + end];
137
138								deps.push(import_path.to_string());
139							}
140						}
141					}
142				}
143
144				// Match importScripts(...)
145				if trimmed.starts_with("importScripts(") {
146					if let Some(paren_start) = trimmed.find('(') {
147						let paren_content = &trimmed[paren_start + 1..];
148
149						if let Some(paren_end) = paren_content.find(')') {
150							let scripts = &paren_content[..paren_end];
151
152							for script in scripts.split(',') {
153								let script = script.trim().trim_matches('"').trim_matches('\'');
154
155								if !script.is_empty() {
156									deps.push(script.to_string());
157								}
158							}
159						}
160					}
161				}
162			}
163		}
164
165		deps
166	}
167}
168
169#[cfg(test)]
170mod tests {
171
172	use super::*;
173
174	#[test]
175	fn test_worker_detection_by_name() {
176		let config = WorkerConfig::new();
177
178		let detector = WorkerDetector::new(config);
179
180		assert!(detector.is_worker_file(Path::new("test.worker.ts")));
181
182		assert!(detector.is_worker_file(Path::new("my-worker.js")));
183
184		assert!(detector.is_worker_file(Path::new("SharedWorker.ts")));
185
186		assert!(!detector.is_worker_file(Path::new("regular.ts")));
187	}
188
189	#[test]
190	fn test_worker_detection_by_content() {
191		let config = WorkerConfig::new();
192
193		let detector = WorkerDetector::new(config);
194
195		let content = r#"
196            self.onmessage = function(e) {
197
198                self.postMessage(e.data);
199            };
200
201        "#;
202
203		assert!(detector.contains_worker_marker(content));
204	}
205
206	#[test]
207	fn test_dependency_extraction() {
208		let config = WorkerConfig::new();
209
210		let detector = WorkerDetector::new(config);
211
212		let path = Path::new("test.worker.ts");
213
214		// This would need an actual file to work properly
215		let _deps = detector.extract_dependencies(path);
216	}
217}