@lithia-js/native 1.0.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/Cargo.toml +31 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/index.d.ts +111 -0
- package/index.js +581 -0
- package/package.json +66 -0
- package/src/builder/compiler.rs +493 -0
- package/src/builder/config.rs +67 -0
- package/src/builder/mod.rs +126 -0
- package/src/builder/sourcemap.rs +36 -0
- package/src/builder/tests.rs +78 -0
- package/src/builder/tsconfig.rs +100 -0
- package/src/builder/types.rs +53 -0
- package/src/events/convention.rs +36 -0
- package/src/events/mod.rs +87 -0
- package/src/events/processor.rs +113 -0
- package/src/events/tests.rs +41 -0
- package/src/events/transformer.rs +78 -0
- package/src/lib.rs +54 -0
- package/src/router/convention.rs +239 -0
- package/src/router/mod.rs +118 -0
- package/src/router/processor.rs +201 -0
- package/src/router/transformer.rs +281 -0
- package/src/scanner/mod.rs +345 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
//! Utilities to transform filesystem route file paths into normalized HTTP
|
|
2
|
+
//! route paths and regular expressions used by the router.
|
|
3
|
+
//!
|
|
4
|
+
//! This module exposes the `PathTransformer` trait which defines the
|
|
5
|
+
//! transformations required to convert a filesystem-style route (for example
|
|
6
|
+
//! `users/[id]/route.ts`) into a normalized runtime path (`/users/:id/route`),
|
|
7
|
+
//! a route regex and helper utilities. The `NativePathTransformer` implements
|
|
8
|
+
//! the trait with the typical behaviour expected by the framework.
|
|
9
|
+
//!
|
|
10
|
+
//! Public API:
|
|
11
|
+
//! - `PathTransformer` trait: abstraction used across the codebase to convert
|
|
12
|
+
//! and normalize paths.
|
|
13
|
+
//! - `NativePathTransformer`: default implementation used by the native router.
|
|
14
|
+
|
|
15
|
+
use regex::Regex;
|
|
16
|
+
|
|
17
|
+
/// Trait that defines path transformation utilities used by the router.
|
|
18
|
+
/// Implementors convert filesystem file paths into normalized route paths,
|
|
19
|
+
/// detect dynamic segments, and produce regular expressions suitable for
|
|
20
|
+
/// matching incoming requests.
|
|
21
|
+
pub trait PathTransformer {
|
|
22
|
+
/// Transform a filesystem file path into a normalized route-like path.
|
|
23
|
+
///
|
|
24
|
+
/// Example: `users/[id]/route.ts` -> `users/:id/route`.
|
|
25
|
+
fn transform_file_path(&self, path: &str) -> String;
|
|
26
|
+
|
|
27
|
+
/// Normalize a route path applying a global prefix, ensuring a leading
|
|
28
|
+
/// slash and removing trailing slashes where appropriate.
|
|
29
|
+
///
|
|
30
|
+
/// Example: `("/users", "/api")` -> `/api/users`.
|
|
31
|
+
fn normalize_path(&self, path: &str, global_prefix: &str) -> String;
|
|
32
|
+
|
|
33
|
+
/// Return true when the supplied path contains dynamic segments (e.g.
|
|
34
|
+
/// `:id`).
|
|
35
|
+
fn is_dynamic_route(&self, path: &str) -> bool;
|
|
36
|
+
|
|
37
|
+
/// Generate a regular expression string from a normalized route path.
|
|
38
|
+
///
|
|
39
|
+
/// Example: `/users/:id` -> `^/users/([^\/]+)$`.
|
|
40
|
+
fn generate_route_regex(&self, path: &str) -> String;
|
|
41
|
+
|
|
42
|
+
/// Clone the transformer as a boxed trait object.
|
|
43
|
+
fn clone_box(&self) -> Box<dyn PathTransformer>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Join `base` and `path`, ensuring there is at most one separator between
|
|
47
|
+
/// them. If `base` is empty, `path` is returned unchanged.
|
|
48
|
+
fn with_base(path: &str, base: &str) -> String {
|
|
49
|
+
if base.is_empty() {
|
|
50
|
+
path.to_string()
|
|
51
|
+
} else {
|
|
52
|
+
format!(
|
|
53
|
+
"{}/{}",
|
|
54
|
+
base.trim_end_matches('/'),
|
|
55
|
+
path.trim_start_matches('/')
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Ensure the provided `path` starts with a leading slash.
|
|
61
|
+
fn with_leading_slash(path: &str) -> String {
|
|
62
|
+
if path.starts_with('/') {
|
|
63
|
+
path.to_string()
|
|
64
|
+
} else {
|
|
65
|
+
format!("/{}", path)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Remove a trailing slash from `path` except when the path is `/`.
|
|
70
|
+
fn without_trailing_slash(path: &str) -> String {
|
|
71
|
+
if path.ends_with('/') && path.len() > 1 {
|
|
72
|
+
path.trim_end_matches('/').to_string()
|
|
73
|
+
} else {
|
|
74
|
+
path.to_string()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Default `PathTransformer` implementation used by the native router.
|
|
79
|
+
/// It converts filesystem route file names and patterns (including dynamic
|
|
80
|
+
/// segments like `[id]` and catch-all `[...path]`) into normalized runtime
|
|
81
|
+
/// path templates and regexes.
|
|
82
|
+
#[derive(Clone)]
|
|
83
|
+
pub struct NativePathTransformer {
|
|
84
|
+
remove_ext: Regex,
|
|
85
|
+
remove_groups: Regex,
|
|
86
|
+
catch_all_named: Regex,
|
|
87
|
+
catch_all: Regex,
|
|
88
|
+
dynamic: Regex,
|
|
89
|
+
dynamic_detector: Regex,
|
|
90
|
+
route_param: Regex,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl NativePathTransformer {
|
|
94
|
+
/// Construct a new `NativePathTransformer` with precompiled regular
|
|
95
|
+
/// expressions tuned for route syntax used by the framework.
|
|
96
|
+
pub fn new() -> Self {
|
|
97
|
+
Self {
|
|
98
|
+
remove_ext: Regex::new(r"\.[A-Za-z]+$").unwrap(),
|
|
99
|
+
remove_groups: Regex::new(r"\(([^(/\\]+)\)[/\\]").unwrap(),
|
|
100
|
+
catch_all_named: Regex::new(r"\[\.\.\.(\w+)\]").unwrap(),
|
|
101
|
+
catch_all: Regex::new(r"\[\.\.\.]").unwrap(),
|
|
102
|
+
dynamic: Regex::new(r"\[([^/\]]+)\]").unwrap(),
|
|
103
|
+
dynamic_detector: Regex::new(r":\w+").unwrap(),
|
|
104
|
+
route_param: Regex::new(r":(\w+)").unwrap(),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl PathTransformer for NativePathTransformer {
|
|
110
|
+
fn transform_file_path(&self, path: &str) -> String {
|
|
111
|
+
let mut result = path.to_string();
|
|
112
|
+
|
|
113
|
+
result = self.remove_ext.replace(&result, "").to_string();
|
|
114
|
+
result = self.remove_groups.replace(&result, "").to_string();
|
|
115
|
+
result = self
|
|
116
|
+
.catch_all_named
|
|
117
|
+
.replace_all(&result, "**:$1")
|
|
118
|
+
.to_string();
|
|
119
|
+
result = self.catch_all.replace_all(&result, "**").to_string();
|
|
120
|
+
result = self.dynamic.replace_all(&result, ":$1").to_string();
|
|
121
|
+
result = result.replace('\\', "/");
|
|
122
|
+
|
|
123
|
+
result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn normalize_path(&self, path: &str, global_prefix: &str) -> String {
|
|
127
|
+
let combined = with_base(path, global_prefix);
|
|
128
|
+
let no_trailing = without_trailing_slash(&combined);
|
|
129
|
+
with_leading_slash(&no_trailing)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn is_dynamic_route(&self, path: &str) -> bool {
|
|
133
|
+
self.dynamic_detector.is_match(path)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn generate_route_regex(&self, path: &str) -> String {
|
|
137
|
+
let escaped = path.replace('/', r"\/");
|
|
138
|
+
|
|
139
|
+
let regex_body = self
|
|
140
|
+
.route_param
|
|
141
|
+
.replace_all(&escaped, r"([^\/]+)")
|
|
142
|
+
.to_string();
|
|
143
|
+
|
|
144
|
+
format!("^{}$", regex_body)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn clone_box(&self) -> Box<dyn PathTransformer> {
|
|
148
|
+
Box::new(self.clone())
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[cfg(test)]
|
|
153
|
+
mod tests {
|
|
154
|
+
use super::*;
|
|
155
|
+
use regex::Regex;
|
|
156
|
+
|
|
157
|
+
fn transformer() -> NativePathTransformer {
|
|
158
|
+
NativePathTransformer::new()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn transform_file_path_removes_extension() {
|
|
163
|
+
let t = transformer();
|
|
164
|
+
assert_eq!(t.transform_file_path("users/route.ts"), "users/route")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn transform_file_path_removes_route_groups() {
|
|
169
|
+
let t = transformer();
|
|
170
|
+
assert_eq!(t.transform_file_path("(v1)/users/route.ts"), "users/route")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn transform_file_path_dynamic_segments() {
|
|
175
|
+
let t = transformer();
|
|
176
|
+
assert_eq!(
|
|
177
|
+
t.transform_file_path("users/[id]/route.ts"),
|
|
178
|
+
"users/:id/route"
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn transform_file_path_multiple_dynamic_segments() {
|
|
184
|
+
let t = transformer();
|
|
185
|
+
assert_eq!(
|
|
186
|
+
t.transform_file_path("users/[userId]/posts/[postId]/route.ts"),
|
|
187
|
+
"users/:userId/posts/:postId/route"
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn transform_file_path_catch_all_named() {
|
|
193
|
+
let t = transformer();
|
|
194
|
+
assert_eq!(
|
|
195
|
+
t.transform_file_path("files/[...path]/route.ts"),
|
|
196
|
+
"files/**:path/route"
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[test]
|
|
201
|
+
fn transform_file_path_catch_all_unnamed() {
|
|
202
|
+
let t = transformer();
|
|
203
|
+
assert_eq!(
|
|
204
|
+
t.transform_file_path("files/[...]/route.ts"),
|
|
205
|
+
"files/**/route"
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn transform_file_path_normalizes_windows_separators() {
|
|
211
|
+
let t = transformer();
|
|
212
|
+
assert_eq!(
|
|
213
|
+
t.transform_file_path(r"users\[id]\route.ts"),
|
|
214
|
+
"users/:id/route"
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn normalize_path_applies_global_prefix() {
|
|
220
|
+
let t = transformer();
|
|
221
|
+
assert_eq!(t.normalize_path("/users", "/api"), "/api/users")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn normalize_path_removes_trailing_slash() {
|
|
226
|
+
let t = transformer();
|
|
227
|
+
assert_eq!(t.normalize_path("/users/", ""), "/users")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn normalize_path_ensures_leading_slash() {
|
|
232
|
+
let t = transformer();
|
|
233
|
+
assert_eq!(t.normalize_path("users", ""), "/users")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
fn detects_dynamic_route() {
|
|
238
|
+
let t = transformer();
|
|
239
|
+
assert!(t.is_dynamic_route("/users/:id"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn detects_static_route() {
|
|
244
|
+
let t = transformer();
|
|
245
|
+
assert!(!t.is_dynamic_route("/about"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn generate_route_regex_static() {
|
|
250
|
+
let t = transformer();
|
|
251
|
+
let regex_str = t.generate_route_regex("/about");
|
|
252
|
+
let regex = Regex::new(®ex_str).unwrap();
|
|
253
|
+
|
|
254
|
+
assert!(regex.is_match("/about"));
|
|
255
|
+
assert!(!regex.is_match("/about/us"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn generate_route_regex_dynamic() {
|
|
260
|
+
let t = transformer();
|
|
261
|
+
let regex_str = t.generate_route_regex("/users/:id");
|
|
262
|
+
let regex = Regex::new(®ex_str).unwrap();
|
|
263
|
+
|
|
264
|
+
assert!(regex.is_match("/users/123"));
|
|
265
|
+
assert!(regex.is_match("/users/abc"));
|
|
266
|
+
assert!(!regex.is_match("/users/"));
|
|
267
|
+
assert!(!regex.is_match("/users/123/profile"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn generate_route_regex_nested_dynamic() {
|
|
272
|
+
let t = transformer();
|
|
273
|
+
let regex_str = t.generate_route_regex("/users/:userId/posts/:postId");
|
|
274
|
+
let regex = Regex::new(®ex_str).unwrap();
|
|
275
|
+
|
|
276
|
+
assert!(regex.is_match("/users/42/posts/100"));
|
|
277
|
+
assert!(regex.is_match("/users/john/posts/abc"));
|
|
278
|
+
assert!(!regex.is_match("/users/42/posts/"));
|
|
279
|
+
assert!(!regex.is_match("/users/42/posts/100/comments"));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
//! Utilities for scanning project directories and matching files using glob patterns.
|
|
2
|
+
//!
|
|
3
|
+
//! This module exposes a small, focused file scanner implemented in Rust that is
|
|
4
|
+
//! used by the native builder to discover route files and other project
|
|
5
|
+
//! artifacts. It provides a simple `FileScanner` trait and a `NativeFileScanner`
|
|
6
|
+
//! concrete implementation that walks directories, applies include/ignore glob
|
|
7
|
+
//! patterns and returns structured `FileInfo` results.
|
|
8
|
+
//!
|
|
9
|
+
//! The scanner intentionally keeps behaviour minimal and deterministic:
|
|
10
|
+
//! - Includes are required: if no include patterns are provided, an empty list
|
|
11
|
+
//! is returned.
|
|
12
|
+
//! - Ignore patterns are optional and applied after include matching.
|
|
13
|
+
//! - Returned file paths are sorted by their relative path to ensure stable
|
|
14
|
+
//! ordering across runs.
|
|
15
|
+
|
|
16
|
+
use globset::{Glob, GlobSetBuilder};
|
|
17
|
+
use napi_derive::napi;
|
|
18
|
+
|
|
19
|
+
use std::io;
|
|
20
|
+
|
|
21
|
+
/// Information about a discovered file.
|
|
22
|
+
/// - `path` is the path relative to the scanned directory (using `/` as
|
|
23
|
+
/// separator on all platforms).
|
|
24
|
+
/// - `full_path` is the absolute filesystem path to the file.
|
|
25
|
+
#[napi(object)]
|
|
26
|
+
#[derive(Debug, Clone)]
|
|
27
|
+
pub struct FileInfo {
|
|
28
|
+
/// Relative path from the scanned directory
|
|
29
|
+
pub path: String,
|
|
30
|
+
|
|
31
|
+
/// Absolute path from the filesystem root
|
|
32
|
+
pub full_path: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Options controlling scanning behaviour.
|
|
36
|
+
/// - `include`: list of glob patterns that select files to include. If
|
|
37
|
+
/// omitted or empty, the scanner returns an empty result set.
|
|
38
|
+
/// - `ignore`: optional list of glob patterns used to exclude matching files
|
|
39
|
+
/// from the previously included set.
|
|
40
|
+
#[napi(object)]
|
|
41
|
+
#[derive(Debug, Clone, Default)]
|
|
42
|
+
pub struct ScanOptions {
|
|
43
|
+
/// Glob patterns to include files.
|
|
44
|
+
pub include: Option<Vec<String>>,
|
|
45
|
+
|
|
46
|
+
/// Glob patterns to ignore (applied after include matching).
|
|
47
|
+
pub ignore: Option<Vec<String>>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Trait that abstracts a directory scanner used by the native build system.
|
|
51
|
+
/// Implementors must return a list of `FileInfo` entries corresponding to the
|
|
52
|
+
/// files discovered under the provided `path_components` directory. This trait
|
|
53
|
+
/// is intentionally small to make testing and mocking straightforward in
|
|
54
|
+
/// higher-level code.
|
|
55
|
+
pub trait FileScanner {
|
|
56
|
+
/// Scan a directory described by `path_components` and return matching
|
|
57
|
+
/// `FileInfo` entries.
|
|
58
|
+
/// `path_components` is a slice of path segments that will be joined onto
|
|
59
|
+
/// the current working directory to form the target scanning directory.
|
|
60
|
+
/// `options` may contain include/ignore glob patterns.
|
|
61
|
+
fn scan_dir(
|
|
62
|
+
&self,
|
|
63
|
+
path_components: &[String],
|
|
64
|
+
options: Option<ScanOptions>,
|
|
65
|
+
) -> io::Result<Vec<FileInfo>>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Native `FileScanner` implementation that walks the filesystem using
|
|
69
|
+
/// `walkdir` and matches paths against glob patterns using `globset`.
|
|
70
|
+
/// This scanner is fast enough for typical project sizes and deterministic
|
|
71
|
+
/// because it sorts results by relative path before returning them.
|
|
72
|
+
#[derive(Debug, Clone)]
|
|
73
|
+
pub struct NativeFileScanner;
|
|
74
|
+
|
|
75
|
+
impl NativeFileScanner {
|
|
76
|
+
/// Create a new `NativeFileScanner` instance.
|
|
77
|
+
pub fn new() -> Self {
|
|
78
|
+
Self
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl FileScanner for NativeFileScanner {
|
|
83
|
+
fn scan_dir(
|
|
84
|
+
&self,
|
|
85
|
+
path_components: &[String],
|
|
86
|
+
options: Option<ScanOptions>,
|
|
87
|
+
) -> io::Result<Vec<FileInfo>> {
|
|
88
|
+
let cwd = std::env::current_dir()?;
|
|
89
|
+
let mut target_path = cwd;
|
|
90
|
+
|
|
91
|
+
for part in path_components {
|
|
92
|
+
target_path = target_path.join(part);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if !target_path.exists() {
|
|
96
|
+
return Err(io::Error::new(
|
|
97
|
+
io::ErrorKind::NotFound,
|
|
98
|
+
format!("Target directory {:?} does not exist", target_path),
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let options = options.unwrap_or_default();
|
|
103
|
+
|
|
104
|
+
let include_patterns = match options.include {
|
|
105
|
+
Some(patterns) if !patterns.is_empty() => patterns,
|
|
106
|
+
_ => return Ok(Vec::new()),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let mut include_builder = GlobSetBuilder::new();
|
|
110
|
+
for pattern in &include_patterns {
|
|
111
|
+
let glob = Glob::new(pattern).map_err(|e| {
|
|
112
|
+
io::Error::new(
|
|
113
|
+
io::ErrorKind::InvalidInput,
|
|
114
|
+
format!("Invalid include glob pattern '{}': {}", pattern, e),
|
|
115
|
+
)
|
|
116
|
+
})?;
|
|
117
|
+
include_builder.add(glob);
|
|
118
|
+
}
|
|
119
|
+
let include_matcher = include_builder.build().map_err(|e| {
|
|
120
|
+
io::Error::new(
|
|
121
|
+
io::ErrorKind::InvalidInput,
|
|
122
|
+
format!("Failed to build include matcher: {}", e),
|
|
123
|
+
)
|
|
124
|
+
})?;
|
|
125
|
+
|
|
126
|
+
let ignore_matcher = if let Some(ignore_patterns) = options.ignore {
|
|
127
|
+
let mut ignore_builder = GlobSetBuilder::new();
|
|
128
|
+
for pattern in &ignore_patterns {
|
|
129
|
+
let glob = Glob::new(pattern).map_err(|e| {
|
|
130
|
+
io::Error::new(
|
|
131
|
+
io::ErrorKind::InvalidInput,
|
|
132
|
+
format!("Invalid ignore glob pattern '{}': {}", pattern, e),
|
|
133
|
+
)
|
|
134
|
+
})?;
|
|
135
|
+
ignore_builder.add(glob);
|
|
136
|
+
}
|
|
137
|
+
Some(ignore_builder.build().map_err(|e| {
|
|
138
|
+
io::Error::new(
|
|
139
|
+
io::ErrorKind::InvalidInput,
|
|
140
|
+
format!("Failed to build ignore matcher: {}", e),
|
|
141
|
+
)
|
|
142
|
+
})?)
|
|
143
|
+
} else {
|
|
144
|
+
None
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let mut file_infos: Vec<FileInfo> = Vec::new();
|
|
148
|
+
|
|
149
|
+
for entry in walkdir::WalkDir::new(&target_path)
|
|
150
|
+
.into_iter()
|
|
151
|
+
.filter_map(|e| e.ok())
|
|
152
|
+
.filter(|e| e.file_type().is_file())
|
|
153
|
+
{
|
|
154
|
+
let path = entry.path();
|
|
155
|
+
let relative = path
|
|
156
|
+
.strip_prefix(&target_path)
|
|
157
|
+
.unwrap_or(path)
|
|
158
|
+
.to_string_lossy()
|
|
159
|
+
.replace('\\', "/");
|
|
160
|
+
|
|
161
|
+
if !include_matcher.is_match(&relative) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if let Some(ref ignore) = ignore_matcher {
|
|
166
|
+
if ignore.is_match(&relative) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
file_infos.push(FileInfo {
|
|
172
|
+
full_path: path.to_string_lossy().to_string(),
|
|
173
|
+
path: relative,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
file_infos.sort_by(|a, b| a.path.cmp(&b.path));
|
|
178
|
+
|
|
179
|
+
Ok(file_infos)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[cfg(test)]
|
|
184
|
+
mod tests {
|
|
185
|
+
use super::*;
|
|
186
|
+
use std::{fs, path::Path};
|
|
187
|
+
use tempfile::TempDir;
|
|
188
|
+
|
|
189
|
+
fn create_test_files(dir: &Path) -> io::Result<()> {
|
|
190
|
+
fs::create_dir_all(dir.join("src"))?;
|
|
191
|
+
fs::create_dir_all(dir.join("dist"))?;
|
|
192
|
+
fs::create_dir_all(dir.join("tests"))?;
|
|
193
|
+
|
|
194
|
+
// TypeScript files
|
|
195
|
+
fs::write(dir.join("src/index.ts"), "export default {}")?;
|
|
196
|
+
fs::write(dir.join("src/utils.ts"), "export const util = 1")?;
|
|
197
|
+
|
|
198
|
+
// JavaScript files
|
|
199
|
+
fs::write(dir.join("src/legacy.js"), "module.exports = {}")?;
|
|
200
|
+
|
|
201
|
+
// Test files
|
|
202
|
+
fs::write(dir.join("tests/index.test.ts"), "test('works', () => {})")?;
|
|
203
|
+
|
|
204
|
+
// JSON files
|
|
205
|
+
fs::write(dir.join("package.json"), "{}")?;
|
|
206
|
+
fs::write(dir.join("tsconfig.json"), "{}")?;
|
|
207
|
+
|
|
208
|
+
// Build output
|
|
209
|
+
fs::write(dir.join("dist/index.js"), "console.log('built')")?;
|
|
210
|
+
|
|
211
|
+
Ok(())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn test_scan_with_default_ts_pattern() {
|
|
216
|
+
let temp_dir = TempDir::new().unwrap();
|
|
217
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
218
|
+
|
|
219
|
+
let scanner = NativeFileScanner::new();
|
|
220
|
+
let result = scanner
|
|
221
|
+
.scan_dir(&[temp_dir.path().to_string_lossy().to_string()], None)
|
|
222
|
+
.unwrap();
|
|
223
|
+
|
|
224
|
+
// Sem include patterns, não deve retornar nada
|
|
225
|
+
assert_eq!(result.len(), 0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn test_scan_typescript_files() {
|
|
230
|
+
let temp_dir = TempDir::new().unwrap();
|
|
231
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
232
|
+
|
|
233
|
+
let scanner = NativeFileScanner::new();
|
|
234
|
+
let result = scanner
|
|
235
|
+
.scan_dir(
|
|
236
|
+
&[temp_dir.path().to_string_lossy().to_string()],
|
|
237
|
+
Some(ScanOptions {
|
|
238
|
+
include: Some(vec!["**/*.ts".to_string()]),
|
|
239
|
+
ignore: None,
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
.unwrap();
|
|
243
|
+
|
|
244
|
+
// Deve encontrar apenas .ts files
|
|
245
|
+
let paths: Vec<_> = result.iter().map(|f| f.path.as_str()).collect();
|
|
246
|
+
assert_eq!(paths.len(), 3);
|
|
247
|
+
assert!(paths.contains(&"src/index.ts"));
|
|
248
|
+
assert!(paths.contains(&"src/utils.ts"));
|
|
249
|
+
assert!(paths.contains(&"tests/index.test.ts"));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn test_scan_with_custom_include_patterns() {
|
|
254
|
+
let temp_dir = TempDir::new().unwrap();
|
|
255
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
256
|
+
|
|
257
|
+
let scanner = NativeFileScanner::new();
|
|
258
|
+
let result = scanner
|
|
259
|
+
.scan_dir(
|
|
260
|
+
&[temp_dir.path().to_string_lossy().to_string()],
|
|
261
|
+
Some(ScanOptions {
|
|
262
|
+
include: Some(vec!["**/*.ts".to_string(), "**/*.js".to_string()]),
|
|
263
|
+
ignore: None,
|
|
264
|
+
}),
|
|
265
|
+
)
|
|
266
|
+
.unwrap();
|
|
267
|
+
|
|
268
|
+
let paths: Vec<_> = result.iter().map(|f| f.path.as_str()).collect();
|
|
269
|
+
assert_eq!(paths.len(), 5); // 3 .ts + 2 .js files
|
|
270
|
+
assert!(paths.contains(&"src/legacy.js"));
|
|
271
|
+
assert!(paths.contains(&"dist/index.js"));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn test_scan_with_ignore_patterns() {
|
|
276
|
+
let temp_dir = TempDir::new().unwrap();
|
|
277
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
278
|
+
|
|
279
|
+
let scanner = NativeFileScanner::new();
|
|
280
|
+
let result = scanner
|
|
281
|
+
.scan_dir(
|
|
282
|
+
&[temp_dir.path().to_string_lossy().to_string()],
|
|
283
|
+
Some(ScanOptions {
|
|
284
|
+
include: Some(vec!["**/*.ts".to_string(), "**/*.js".to_string()]),
|
|
285
|
+
ignore: Some(vec!["**/*.test.ts".to_string(), "**/dist/**".to_string()]),
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
.unwrap();
|
|
289
|
+
|
|
290
|
+
let paths: Vec<_> = result.iter().map(|f| f.path.as_str()).collect();
|
|
291
|
+
// Should exclude test files and dist folder
|
|
292
|
+
assert_eq!(paths.len(), 3);
|
|
293
|
+
assert!(paths.contains(&"src/index.ts"));
|
|
294
|
+
assert!(paths.contains(&"src/utils.ts"));
|
|
295
|
+
assert!(paths.contains(&"src/legacy.js"));
|
|
296
|
+
assert!(!paths.contains(&"tests/index.test.ts"));
|
|
297
|
+
assert!(!paths.contains(&"dist/index.js"));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_scan_json_files_only() {
|
|
302
|
+
let temp_dir = TempDir::new().unwrap();
|
|
303
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
304
|
+
|
|
305
|
+
let scanner = NativeFileScanner::new();
|
|
306
|
+
let result = scanner
|
|
307
|
+
.scan_dir(
|
|
308
|
+
&[temp_dir.path().to_string_lossy().to_string()],
|
|
309
|
+
Some(ScanOptions {
|
|
310
|
+
include: Some(vec!["**/*.json".to_string()]),
|
|
311
|
+
ignore: None,
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
.unwrap();
|
|
315
|
+
|
|
316
|
+
let paths: Vec<_> = result.iter().map(|f| f.path.as_str()).collect();
|
|
317
|
+
assert_eq!(paths.len(), 2);
|
|
318
|
+
assert!(paths.contains(&"package.json"));
|
|
319
|
+
assert!(paths.contains(&"tsconfig.json"));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#[test]
|
|
323
|
+
fn test_scan_specific_directory_with_globs() {
|
|
324
|
+
let temp_dir = TempDir::new().unwrap();
|
|
325
|
+
create_test_files(temp_dir.path()).unwrap();
|
|
326
|
+
|
|
327
|
+
let scanner = NativeFileScanner::new();
|
|
328
|
+
let result = scanner
|
|
329
|
+
.scan_dir(
|
|
330
|
+
&[temp_dir.path().to_string_lossy().to_string()],
|
|
331
|
+
Some(ScanOptions {
|
|
332
|
+
include: Some(vec!["src/**/*.ts".to_string()]),
|
|
333
|
+
ignore: None,
|
|
334
|
+
}),
|
|
335
|
+
)
|
|
336
|
+
.unwrap();
|
|
337
|
+
|
|
338
|
+
let paths: Vec<_> = result.iter().map(|f| f.path.as_str()).collect();
|
|
339
|
+
// Only src/ directory .ts files
|
|
340
|
+
assert_eq!(paths.len(), 2);
|
|
341
|
+
assert!(paths.contains(&"src/index.ts"));
|
|
342
|
+
assert!(paths.contains(&"src/utils.ts"));
|
|
343
|
+
assert!(!paths.contains(&"tests/index.test.ts"));
|
|
344
|
+
}
|
|
345
|
+
}
|