@lithia-js/native 1.0.0-canary.2 → 1.0.0-canary.3
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 +6 -0
- package/package.json +14 -14
- package/src/builder/compiler.rs +0 -493
- package/src/builder/config.rs +0 -67
- package/src/builder/mod.rs +0 -126
- package/src/builder/sourcemap.rs +0 -36
- package/src/builder/tests.rs +0 -78
- package/src/builder/tsconfig.rs +0 -100
- package/src/builder/types.rs +0 -53
- package/src/events/convention.rs +0 -36
- package/src/events/mod.rs +0 -87
- package/src/events/processor.rs +0 -113
- package/src/events/tests.rs +0 -41
- package/src/events/transformer.rs +0 -78
- package/src/lib.rs +0 -54
- package/src/router/convention.rs +0 -239
- package/src/router/mod.rs +0 -118
- package/src/router/processor.rs +0 -201
- package/src/router/transformer.rs +0 -281
- package/src/scanner/mod.rs +0 -345
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
use regex::Regex;
|
|
2
|
-
|
|
3
|
-
pub trait PathTransformer {
|
|
4
|
-
/// Normalize a file path by removing extensions, groups, and normalizing
|
|
5
|
-
/// separators.
|
|
6
|
-
fn normalize(&self, path: &str) -> String;
|
|
7
|
-
|
|
8
|
-
/// Normalize a path and apply an optional global prefix, returning a
|
|
9
|
-
/// value that always starts with a leading slash.
|
|
10
|
-
fn normalize_path(&self, path: &str, global_prefix: &str) -> String;
|
|
11
|
-
|
|
12
|
-
/// Clone the transformer as a boxed trait object.
|
|
13
|
-
fn clone_box(&self) -> Box<dyn PathTransformer>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
#[derive(Clone)]
|
|
17
|
-
pub struct NativeEventTransformer {
|
|
18
|
-
remove_ext: Regex,
|
|
19
|
-
remove_groups: Regex,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
impl NativeEventTransformer {
|
|
23
|
-
pub fn new() -> Self {
|
|
24
|
-
Self {
|
|
25
|
-
remove_ext: Regex::new(r"\.[A-Za-z0-9]+$").unwrap(),
|
|
26
|
-
remove_groups: Regex::new(r"\(([^(/\\]+)\)[/\\]").unwrap(),
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
impl PathTransformer for NativeEventTransformer {
|
|
32
|
-
/// Normalize an event file path: remove extension, remove groups and
|
|
33
|
-
/// normalize separators to `/`.
|
|
34
|
-
fn normalize(&self, path: &str) -> String {
|
|
35
|
-
let mut s = path.to_string();
|
|
36
|
-
s = self.remove_ext.replace(&s, "").to_string();
|
|
37
|
-
s = self.remove_groups.replace_all(&s, "").to_string();
|
|
38
|
-
s.replace('\\', "/")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/// Normalize a path and apply an optional global prefix, returning a
|
|
42
|
-
/// value that always starts with a leading slash.
|
|
43
|
-
fn normalize_path(&self, path: &str, global_prefix: &str) -> String {
|
|
44
|
-
let combined = if global_prefix.is_empty() {
|
|
45
|
-
path.to_string()
|
|
46
|
-
} else {
|
|
47
|
-
format!("{}/{}", global_prefix.trim_end_matches('/'), path.trim_start_matches('/'))
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
let no_trailing = if combined.ends_with('/') && combined.len() > 1 {
|
|
51
|
-
combined.trim_end_matches('/').to_string()
|
|
52
|
-
} else {
|
|
53
|
-
combined
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
if no_trailing.starts_with('/') {
|
|
57
|
-
no_trailing
|
|
58
|
-
} else {
|
|
59
|
-
format!("/{}", no_trailing)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
fn clone_box(&self) -> Box<dyn PathTransformer> {
|
|
64
|
-
Box::new(self.clone())
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
#[cfg(test)]
|
|
69
|
-
mod tests {
|
|
70
|
-
use super::*;
|
|
71
|
-
|
|
72
|
-
#[test]
|
|
73
|
-
fn normalize_basic() {
|
|
74
|
-
let t = NativeEventTransformer::new();
|
|
75
|
-
assert_eq!(t.normalize("app/events/chat/message.ts"), "app/events/chat/message");
|
|
76
|
-
assert_eq!(t.normalize("(v1)/events/connection.ts"), "events/connection");
|
|
77
|
-
}
|
|
78
|
-
}
|
package/src/lib.rs
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
//! N-API bindings exported by the native crate.
|
|
2
|
-
//!
|
|
3
|
-
//! This module exposes a small surface area to the host (Node) so JavaScript
|
|
4
|
-
//! code can invoke the native builder and scanner implemented in Rust.
|
|
5
|
-
//! Prefer using the higher-level functions exported here rather than calling
|
|
6
|
-
//! into the modules directly from the host.
|
|
7
|
-
|
|
8
|
-
use napi_derive::napi;
|
|
9
|
-
|
|
10
|
-
mod builder;
|
|
11
|
-
mod router;
|
|
12
|
-
mod scanner;
|
|
13
|
-
mod events;
|
|
14
|
-
|
|
15
|
-
/// Compile the project and emit artifacts.
|
|
16
|
-
///
|
|
17
|
-
/// This re-export exposes the native `build_project` entrypoint implemented
|
|
18
|
-
/// in the `builder` module. The function is intended to be invoked from the
|
|
19
|
-
/// host via N-API and performs scanning, compilation and manifest emission.
|
|
20
|
-
pub use builder::build_project;
|
|
21
|
-
|
|
22
|
-
/// Route and manifest types produced by the native router processor.
|
|
23
|
-
pub use router::{Route, RoutesManifest};
|
|
24
|
-
|
|
25
|
-
/// Scanner types returned by `scan_dir`.
|
|
26
|
-
pub use scanner::{FileInfo, ScanOptions};
|
|
27
|
-
|
|
28
|
-
use crate::scanner::FileScanner;
|
|
29
|
-
|
|
30
|
-
/// Scan a directory tree for files matching the provided `path_components`.
|
|
31
|
-
///
|
|
32
|
-
/// `path_components` is a vector of path segments used as roots for the
|
|
33
|
-
/// scanner (for example `["examples/1-basic-api"]`). `options` may include
|
|
34
|
-
/// include/ignore globs. Returns a list of `FileInfo` describing discovered
|
|
35
|
-
/// files or an error which is converted into a `napi::Error` for the host.
|
|
36
|
-
#[napi]
|
|
37
|
-
pub fn scan_dir(
|
|
38
|
-
path_components: Vec<String>,
|
|
39
|
-
options: Option<ScanOptions>,
|
|
40
|
-
) -> napi::Result<Vec<FileInfo>> {
|
|
41
|
-
let scanner = scanner::NativeFileScanner::new();
|
|
42
|
-
scanner
|
|
43
|
-
.scan_dir(&path_components, options)
|
|
44
|
-
.map_err(|e| napi::Error::from_reason(format!("scan failed: {}", e)))
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// Return the native crate version embedded at compile time.
|
|
48
|
-
///
|
|
49
|
-
/// This returns the value of `CARGO_PKG_VERSION` so the host can verify the
|
|
50
|
-
/// native binary version matches expectations.
|
|
51
|
-
#[napi]
|
|
52
|
-
pub fn schema_version() -> &'static str {
|
|
53
|
-
env!("CARGO_PKG_VERSION")
|
|
54
|
-
}
|
package/src/router/convention.rs
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
//! Route filename conventions and helpers.
|
|
2
|
-
//!
|
|
3
|
-
//! This module defines how route filenames are interpreted by the framework.
|
|
4
|
-
//! It provides:
|
|
5
|
-
//! - `RouteConvention` trait used to normalize filesystem paths and extract
|
|
6
|
-
//! optional HTTP method suffixes.
|
|
7
|
-
//! - `NativeRouteConvention` default implementation that follows the project's
|
|
8
|
-
//! filename conventions (e.g. `route.get.ts`, route groups, dynamic
|
|
9
|
-
//! segments).
|
|
10
|
-
//!
|
|
11
|
-
//! The convention is intentionally small and pluggable so the routing
|
|
12
|
-
//! behaviour can be adapted for different project layouts.
|
|
13
|
-
|
|
14
|
-
use regex::Regex;
|
|
15
|
-
|
|
16
|
-
use crate::router::transformer::{NativePathTransformer, PathTransformer};
|
|
17
|
-
|
|
18
|
-
/// Trait that defines how filenames are converted into runtime route paths.
|
|
19
|
-
/// Implementors should remove route-specific filename suffixes and perform
|
|
20
|
-
/// any normalization required before the `PathTransformer` is applied.
|
|
21
|
-
pub trait RouteConvention {
|
|
22
|
-
/// Transform a filesystem location into a normalized route path.
|
|
23
|
-
///
|
|
24
|
-
/// The returned path should include a leading `/`.
|
|
25
|
-
fn transform_path(&self, path: &str) -> String;
|
|
26
|
-
|
|
27
|
-
/// Extract an optional HTTP method suffix from a filename and return the
|
|
28
|
-
/// sanitized path alongside the detected method.
|
|
29
|
-
fn extract_method(&self, path: &str) -> ExtractedMethod;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// Supported HTTP method suffixes that can be encoded in filenames.
|
|
33
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
34
|
-
pub enum MatchedMethodSuffix {
|
|
35
|
-
Delete,
|
|
36
|
-
Get,
|
|
37
|
-
Head,
|
|
38
|
-
Options,
|
|
39
|
-
Patch,
|
|
40
|
-
Post,
|
|
41
|
-
Put,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
impl MatchedMethodSuffix {
|
|
45
|
-
/// Parse a method name (case-insensitive) into a `MatchedMethodSuffix`.
|
|
46
|
-
pub fn from_str(method: &str) -> Option<Self> {
|
|
47
|
-
match method.to_lowercase().as_str() {
|
|
48
|
-
"delete" => Some(MatchedMethodSuffix::Delete),
|
|
49
|
-
"get" => Some(MatchedMethodSuffix::Get),
|
|
50
|
-
"head" => Some(MatchedMethodSuffix::Head),
|
|
51
|
-
"options" => Some(MatchedMethodSuffix::Options),
|
|
52
|
-
"patch" => Some(MatchedMethodSuffix::Patch),
|
|
53
|
-
"post" => Some(MatchedMethodSuffix::Post),
|
|
54
|
-
"put" => Some(MatchedMethodSuffix::Put),
|
|
55
|
-
_ => None,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/// Return the uppercase HTTP method string (e.g. `GET`).
|
|
60
|
-
pub fn as_str(&self) -> &'static str {
|
|
61
|
-
match self {
|
|
62
|
-
MatchedMethodSuffix::Delete => "DELETE",
|
|
63
|
-
MatchedMethodSuffix::Get => "GET",
|
|
64
|
-
MatchedMethodSuffix::Head => "HEAD",
|
|
65
|
-
MatchedMethodSuffix::Options => "OPTIONS",
|
|
66
|
-
MatchedMethodSuffix::Patch => "PATCH",
|
|
67
|
-
MatchedMethodSuffix::Post => "POST",
|
|
68
|
-
MatchedMethodSuffix::Put => "PUT",
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/// Result of extracting an optional method suffix from a filename.
|
|
74
|
-
#[derive(Debug)]
|
|
75
|
-
pub struct ExtractedMethod {
|
|
76
|
-
/// Detected method if the filename contained a method suffix.
|
|
77
|
-
pub method: Option<MatchedMethodSuffix>,
|
|
78
|
-
|
|
79
|
-
/// The sanitized path with the method suffix removed. Always starts with
|
|
80
|
-
/// a leading `/`.
|
|
81
|
-
pub updated_path: String,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// Default route filename convention implementation.
|
|
85
|
-
/// Recognizes filenames like `/users/route.ts` and `/users/route.post.ts` and
|
|
86
|
-
/// removes the convention-specific suffixes. It delegates general path
|
|
87
|
-
/// transformations (groups, dynamic segments, extension removal) to a
|
|
88
|
-
/// `PathTransformer`.
|
|
89
|
-
pub struct NativeRouteConvention {
|
|
90
|
-
route_regex: Regex,
|
|
91
|
-
transformer: Box<dyn PathTransformer>,
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
impl NativeRouteConvention {
|
|
95
|
-
/// Create a new `NativeRouteConvention`.
|
|
96
|
-
/// `transformer` can be used to inject a custom `PathTransformer`; if
|
|
97
|
-
/// omitted the default `NativePathTransformer` is used.
|
|
98
|
-
pub fn new(transformer: Option<Box<dyn PathTransformer>>) -> Self {
|
|
99
|
-
Self {
|
|
100
|
-
// Allow matching "route.get.ts" at the start of string OR after a slash
|
|
101
|
-
route_regex: Regex::new(
|
|
102
|
-
r"(^|/)route(\.(delete|get|head|options|patch|post|put))?\.(ts|js)$",
|
|
103
|
-
)
|
|
104
|
-
.unwrap(),
|
|
105
|
-
transformer: transformer.unwrap_or_else(|| Box::new(NativePathTransformer::new())),
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
impl RouteConvention for NativeRouteConvention {
|
|
111
|
-
fn transform_path(&self, path: &str) -> String {
|
|
112
|
-
let mut result = path.to_string();
|
|
113
|
-
|
|
114
|
-
// Normalize to use forward slashes first
|
|
115
|
-
result = result.replace('\\', "/");
|
|
116
|
-
|
|
117
|
-
// Remove /route.ts and /route.{method}.ts (convention-specific)
|
|
118
|
-
result = self.route_regex.replace(&result, "").to_string();
|
|
119
|
-
|
|
120
|
-
// Delegate generic transformations to transformer
|
|
121
|
-
result = self.transformer.transform_file_path(&result);
|
|
122
|
-
|
|
123
|
-
// Ensure leading slash
|
|
124
|
-
if !result.starts_with('/') {
|
|
125
|
-
result = format!("/{}", result);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
result
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
fn extract_method(&self, path: &str) -> ExtractedMethod {
|
|
132
|
-
if let Some(caps) = self.route_regex.captures(path) {
|
|
133
|
-
let method = caps
|
|
134
|
-
.get(3) // Capture group 3 is the method name (without dot)
|
|
135
|
-
.and_then(|m| MatchedMethodSuffix::from_str(m.as_str()));
|
|
136
|
-
|
|
137
|
-
let mut updated_path = self.route_regex.replace(path, "").to_string();
|
|
138
|
-
|
|
139
|
-
// Ensure leading slash
|
|
140
|
-
if !updated_path.starts_with('/') {
|
|
141
|
-
updated_path = format!("/{}", updated_path);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return ExtractedMethod { method, updated_path };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let mut updated_path = path.to_string();
|
|
148
|
-
|
|
149
|
-
// Ensure leading slash
|
|
150
|
-
if !updated_path.starts_with('/') {
|
|
151
|
-
updated_path = format!("/{}", updated_path);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
ExtractedMethod { method: None, updated_path }
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
#[cfg(test)]
|
|
159
|
-
mod tests {
|
|
160
|
-
use super::*;
|
|
161
|
-
|
|
162
|
-
fn convention() -> NativeRouteConvention {
|
|
163
|
-
NativeRouteConvention::new(None)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
#[test]
|
|
167
|
-
fn transform_removes_route_ts() {
|
|
168
|
-
let c = convention();
|
|
169
|
-
assert_eq!(c.transform_path("users/route.ts"), "/users");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
#[test]
|
|
173
|
-
fn transform_removes_method_suffix() {
|
|
174
|
-
let c = convention();
|
|
175
|
-
assert_eq!(c.transform_path("users/route.get.ts"), "/users");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
#[test]
|
|
179
|
-
fn transform_removes_route_groups() {
|
|
180
|
-
let c = convention();
|
|
181
|
-
assert_eq!(c.transform_path("(v1)/users/route.ts"), "/users");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
#[test]
|
|
185
|
-
fn transform_dynamic_segments() {
|
|
186
|
-
let c = convention();
|
|
187
|
-
assert_eq!(c.transform_path("users/[id]/route.ts"), "/users/:id");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
#[test]
|
|
191
|
-
fn transform_normalizes_windows_separators() {
|
|
192
|
-
let c = convention();
|
|
193
|
-
assert_eq!(c.transform_path(r"users\[id]\route.ts"), "/users/:id");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
#[test]
|
|
197
|
-
fn transform_ensures_leading_slash() {
|
|
198
|
-
let c = convention();
|
|
199
|
-
assert_eq!(c.transform_path("users/route.ts"), "/users");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
#[test]
|
|
203
|
-
fn extract_method_from_route_filename() {
|
|
204
|
-
let c = convention();
|
|
205
|
-
let extracted = c.extract_method("users/route.post.ts");
|
|
206
|
-
assert_eq!(extracted.method, Some(MatchedMethodSuffix::Post));
|
|
207
|
-
assert_eq!(extracted.updated_path, "/users");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
#[test]
|
|
211
|
-
fn extract_method_without_method_suffix() {
|
|
212
|
-
let c = convention();
|
|
213
|
-
let extracted = c.extract_method("users/route.ts");
|
|
214
|
-
assert_eq!(extracted.method, None);
|
|
215
|
-
assert_eq!(extracted.updated_path, "/users");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
#[test]
|
|
219
|
-
fn extract_then_transform_produces_clean_route() {
|
|
220
|
-
let c = convention();
|
|
221
|
-
|
|
222
|
-
let extracted = c.extract_method("/(auth)/users/[id]/route.put.ts");
|
|
223
|
-
let path = c.transform_path(&extracted.updated_path);
|
|
224
|
-
|
|
225
|
-
assert_eq!(extracted.method, Some(MatchedMethodSuffix::Put));
|
|
226
|
-
assert_eq!(path, "/users/:id");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#[test]
|
|
230
|
-
fn full_static_route_pipeline() {
|
|
231
|
-
let c = convention();
|
|
232
|
-
|
|
233
|
-
let extracted = c.extract_method("/health/route.ts");
|
|
234
|
-
let path = c.transform_path(&extracted.updated_path);
|
|
235
|
-
|
|
236
|
-
assert_eq!(extracted.method, None);
|
|
237
|
-
assert_eq!(path, "/health");
|
|
238
|
-
}
|
|
239
|
-
}
|
package/src/router/mod.rs
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
//! Router core types and interop structures.
|
|
2
|
-
//!
|
|
3
|
-
//! This module defines the small set of types used by the router and the
|
|
4
|
-
//! native build pipeline to represent discovered routes and the manifest
|
|
5
|
-
//! exported to the host environment (Node). It exposes:
|
|
6
|
-
//! - `Route`: serializable representation exposed to JavaScript via N-API.
|
|
7
|
-
//! - `RoutesManifest`: top-level manifest object sent to the host.
|
|
8
|
-
//! - `RouteCore`: internal representation used inside the Rust codebase.
|
|
9
|
-
|
|
10
|
-
use std::fs;
|
|
11
|
-
|
|
12
|
-
use napi_derive::napi;
|
|
13
|
-
use serde::Serialize;
|
|
14
|
-
|
|
15
|
-
use crate::{builder::config::BuildConfig, router::{convention::MatchedMethodSuffix, processor::{NativeRouteProcessor, RouteProcessor}}, scanner::FileScanner, schema_version};
|
|
16
|
-
|
|
17
|
-
pub mod convention;
|
|
18
|
-
pub mod processor;
|
|
19
|
-
pub mod transformer;
|
|
20
|
-
|
|
21
|
-
/// Serializable route representation sent to the host (N-API).
|
|
22
|
-
/// Fields are camel-cased to be idiomatic on the JavaScript side.
|
|
23
|
-
#[napi(object)]
|
|
24
|
-
#[derive(Serialize)]
|
|
25
|
-
#[serde(rename_all = "camelCase")]
|
|
26
|
-
pub struct Route {
|
|
27
|
-
/// Uppercase HTTP method name, e.g. `GET` or `POST`, when present.
|
|
28
|
-
pub method: Option<String>,
|
|
29
|
-
|
|
30
|
-
/// Normalized route path (always starting with `/`).
|
|
31
|
-
pub path: String,
|
|
32
|
-
|
|
33
|
-
/// True when the route contains dynamic segments.
|
|
34
|
-
pub dynamic: bool,
|
|
35
|
-
|
|
36
|
-
/// Absolute filesystem path to the source file backing the route.
|
|
37
|
-
pub file_path: String,
|
|
38
|
-
|
|
39
|
-
/// Generated route matching regex as a string.
|
|
40
|
-
pub regex: String,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// Manifest containing all discovered routes, serializable to the host.
|
|
44
|
-
#[napi(object)]
|
|
45
|
-
#[derive(Serialize)]
|
|
46
|
-
pub struct RoutesManifest {
|
|
47
|
-
/// Manifest version string.
|
|
48
|
-
pub version: String,
|
|
49
|
-
|
|
50
|
-
/// List of routes.
|
|
51
|
-
pub routes: Vec<Route>,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// Internal representation of a route used within Rust code.
|
|
55
|
-
/// `RouteCore` contains a typed `MatchedMethodSuffix` for internal routing
|
|
56
|
-
/// logic; it is converted to the exported `Route` when communicating with the
|
|
57
|
-
/// host.
|
|
58
|
-
pub struct RouteCore {
|
|
59
|
-
pub method: Option<MatchedMethodSuffix>,
|
|
60
|
-
pub path: String,
|
|
61
|
-
pub dynamic: bool,
|
|
62
|
-
pub file_path: String,
|
|
63
|
-
pub regex: String,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
impl From<RouteCore> for Route {
|
|
67
|
-
fn from(core: RouteCore) -> Self {
|
|
68
|
-
Self {
|
|
69
|
-
method: core.method.map(|m| m.as_str().to_string()),
|
|
70
|
-
path: core.path,
|
|
71
|
-
dynamic: core.dynamic,
|
|
72
|
-
file_path: core.file_path,
|
|
73
|
-
regex: core.regex,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pub fn write_routes_manifest(config: &BuildConfig) -> Result<(), String> {
|
|
80
|
-
let routes_path = config.out_root.join("app").join("routes");
|
|
81
|
-
if !routes_path.exists() {
|
|
82
|
-
return Ok(());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let route_files = crate::scanner::NativeFileScanner::new()
|
|
86
|
-
.scan_dir(
|
|
87
|
-
&[
|
|
88
|
-
config.output_path_str(),
|
|
89
|
-
"app".to_string(),
|
|
90
|
-
"routes".to_string(),
|
|
91
|
-
],
|
|
92
|
-
Some(crate::scanner::ScanOptions {
|
|
93
|
-
include: Some(vec!["**/*.js".to_string()]),
|
|
94
|
-
ignore: None,
|
|
95
|
-
}),
|
|
96
|
-
)
|
|
97
|
-
.map_err(|e| format!("scan failed: {}", e))?;
|
|
98
|
-
|
|
99
|
-
let processor = NativeRouteProcessor::new(None, None);
|
|
100
|
-
|
|
101
|
-
let version = schema_version().to_string();
|
|
102
|
-
let routes: Vec<Route> = route_files
|
|
103
|
-
.iter()
|
|
104
|
-
.map(|file| processor.process_route_file(file))
|
|
105
|
-
.filter(|route| route.method.is_some())
|
|
106
|
-
.map(Route::from)
|
|
107
|
-
.collect();
|
|
108
|
-
|
|
109
|
-
let manifest = RoutesManifest { version, routes };
|
|
110
|
-
|
|
111
|
-
let json = serde_json::to_string(&manifest)
|
|
112
|
-
.map_err(|e| format!("Failed to serialize routes: {}", e))?;
|
|
113
|
-
|
|
114
|
-
fs::write(config.out_root.join("routes.json"), json)
|
|
115
|
-
.map_err(|e| format!("Failed to write file: {}", e))?;
|
|
116
|
-
|
|
117
|
-
Ok(())
|
|
118
|
-
}
|
package/src/router/processor.rs
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
//! Route file processor utilities.
|
|
2
|
-
//!
|
|
3
|
-
//! This module exposes the `RouteProcessor` trait and a concrete
|
|
4
|
-
//! `NativeRouteProcessor` implementation. The processor is responsible for
|
|
5
|
-
//! converting a scanned filesystem `FileInfo` (a file that follows the route
|
|
6
|
-
//! file conventions) into a `RouteCore` structure used by the router.
|
|
7
|
-
//!
|
|
8
|
-
//! Behaviour highlights:
|
|
9
|
-
//! - Extracts optional HTTP method suffix (e.g. `route.post.ts`).
|
|
10
|
-
//! - Transforms filesystem path patterns into normalized runtime paths
|
|
11
|
-
//! (e.g. `users/[id]/route.ts` -> `/users/:id`).
|
|
12
|
-
//! - Removes trailing `/index` segments and ensures leading slash.
|
|
13
|
-
//! - Produces a regex string for route matching and indicates whether a
|
|
14
|
-
//! route is dynamic.
|
|
15
|
-
|
|
16
|
-
use crate::scanner::FileInfo;
|
|
17
|
-
use crate::router::{
|
|
18
|
-
convention::{NativeRouteConvention, RouteConvention},
|
|
19
|
-
transformer::{NativePathTransformer, PathTransformer},
|
|
20
|
-
RouteCore,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/// Trait that converts a discovered `FileInfo` into framework `RouteCore`.
|
|
24
|
-
/// Implementations encapsulate how filenames and paths are interpreted as
|
|
25
|
-
/// runtime routes.
|
|
26
|
-
pub trait RouteProcessor {
|
|
27
|
-
/// Process a scanned `file` and return a `RouteCore` describing the
|
|
28
|
-
/// route's HTTP method (if any), normalized path, whether it's dynamic,
|
|
29
|
-
/// the original file path and the generated regex.
|
|
30
|
-
fn process_route_file(&self, file: &FileInfo) -> RouteCore;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/// Native implementation of `RouteProcessor` used by the default router.
|
|
34
|
-
/// `NativeRouteProcessor` composes a `PathTransformer` and a
|
|
35
|
-
/// `RouteConvention` to implement the file -> route conversion logic. Both
|
|
36
|
-
/// components are replaceable for testing or customization.
|
|
37
|
-
pub struct NativeRouteProcessor {
|
|
38
|
-
transformer: Box<dyn PathTransformer>,
|
|
39
|
-
convention: Box<dyn RouteConvention>,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
impl NativeRouteProcessor {
|
|
43
|
-
/// Create a new `NativeRouteProcessor`.
|
|
44
|
-
/// `opt_transformer` and `opt_convention` are optional boxed trait
|
|
45
|
-
/// instances. When omitted, default native implementations are used.
|
|
46
|
-
pub fn new(
|
|
47
|
-
opt_transformer: Option<Box<dyn PathTransformer>>,
|
|
48
|
-
opt_convention: Option<Box<dyn RouteConvention>>,
|
|
49
|
-
) -> Self {
|
|
50
|
-
let transformer = opt_transformer.unwrap_or_else(|| Box::new(NativePathTransformer::new()));
|
|
51
|
-
let convention = opt_convention
|
|
52
|
-
.unwrap_or_else(|| Box::new(NativeRouteConvention::new(Some(transformer.clone_box()))));
|
|
53
|
-
|
|
54
|
-
Self {
|
|
55
|
-
transformer,
|
|
56
|
-
convention,
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
impl RouteProcessor for NativeRouteProcessor {
|
|
62
|
-
/// Convert a filesystem `FileInfo` into a `RouteCore`.
|
|
63
|
-
/// Steps:
|
|
64
|
-
/// 1. Use the `RouteConvention` to extract any method suffix and the
|
|
65
|
-
/// canonical path.
|
|
66
|
-
/// 2. Normalize the path (apply prefix, remove trailing `/index`, ensure
|
|
67
|
-
/// leading slash).
|
|
68
|
-
/// 3. Determine whether the route is dynamic and generate the route regex.
|
|
69
|
-
fn process_route_file(&self, file: &FileInfo) -> RouteCore {
|
|
70
|
-
let extracted = self.convention.extract_method(&file.path);
|
|
71
|
-
let mut path = self.convention.transform_path(&extracted.updated_path);
|
|
72
|
-
|
|
73
|
-
path = self.transformer.normalize_path(&path, "");
|
|
74
|
-
|
|
75
|
-
let dynamic = self.transformer.is_dynamic_route(&path);
|
|
76
|
-
let regex = self.transformer.generate_route_regex(&path);
|
|
77
|
-
|
|
78
|
-
RouteCore {
|
|
79
|
-
method: extracted.method,
|
|
80
|
-
path,
|
|
81
|
-
dynamic,
|
|
82
|
-
file_path: file.full_path.clone(),
|
|
83
|
-
regex,
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
#[cfg(test)]
|
|
89
|
-
mod tests {
|
|
90
|
-
use super::*;
|
|
91
|
-
use crate::router::convention::MatchedMethodSuffix;
|
|
92
|
-
|
|
93
|
-
fn processor() -> NativeRouteProcessor {
|
|
94
|
-
NativeRouteProcessor::new(None, None)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fn file_info(path: &str, full_path: &str) -> FileInfo {
|
|
98
|
-
FileInfo {
|
|
99
|
-
path: path.to_string(),
|
|
100
|
-
full_path: full_path.to_string(),
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
#[test]
|
|
105
|
-
fn processes_static_routes_with_methods() {
|
|
106
|
-
let p = processor();
|
|
107
|
-
|
|
108
|
-
// Simple route without method
|
|
109
|
-
let route = p.process_route_file(&file_info("users/route.ts", "/project/src/users/route.ts"));
|
|
110
|
-
assert_eq!(route.path, "/users");
|
|
111
|
-
assert_eq!(route.method, None);
|
|
112
|
-
assert!(!route.dynamic);
|
|
113
|
-
assert_eq!(route.file_path, "/project/src/users/route.ts");
|
|
114
|
-
assert_eq!(route.regex, r"^\/users$");
|
|
115
|
-
|
|
116
|
-
// Route with POST method
|
|
117
|
-
let route = p.process_route_file(&file_info("users/route.post.ts", "/project/src/users/route.post.ts"));
|
|
118
|
-
assert_eq!(route.path, "/users");
|
|
119
|
-
assert_eq!(route.method, Some(MatchedMethodSuffix::Post));
|
|
120
|
-
assert!(!route.dynamic);
|
|
121
|
-
assert_eq!(route.file_path, "/project/src/users/route.post.ts");
|
|
122
|
-
|
|
123
|
-
// Route with GET method
|
|
124
|
-
let route = p.process_route_file(&file_info("about/route.get.ts", "/project/src/about/route.get.ts"));
|
|
125
|
-
assert_eq!(route.path, "/about");
|
|
126
|
-
assert_eq!(route.method, Some(MatchedMethodSuffix::Get));
|
|
127
|
-
assert_eq!(route.regex, r"^\/about$");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#[test]
|
|
131
|
-
fn processes_dynamic_routes() {
|
|
132
|
-
let p = processor();
|
|
133
|
-
|
|
134
|
-
// Single dynamic segment
|
|
135
|
-
let route = p.process_route_file(&file_info("users/[id]/route.ts", "/project/src/users/[id]/route.ts"));
|
|
136
|
-
assert_eq!(route.path, "/users/:id");
|
|
137
|
-
assert_eq!(route.method, None);
|
|
138
|
-
assert!(route.dynamic);
|
|
139
|
-
assert_eq!(route.file_path, "/project/src/users/[id]/route.ts");
|
|
140
|
-
assert_eq!(route.regex, r"^\/users\/([^\/]+)$");
|
|
141
|
-
|
|
142
|
-
// Multiple dynamic segments with method
|
|
143
|
-
let route = p.process_route_file(&file_info(
|
|
144
|
-
"users/[userId]/posts/[postId]/route.get.ts",
|
|
145
|
-
"/project/src/users/[userId]/posts/[postId]/route.get.ts",
|
|
146
|
-
));
|
|
147
|
-
assert_eq!(route.path, "/users/:userId/posts/:postId");
|
|
148
|
-
assert_eq!(route.method, Some(MatchedMethodSuffix::Get));
|
|
149
|
-
assert!(route.dynamic);
|
|
150
|
-
assert_eq!(route.file_path, "/project/src/users/[userId]/posts/[postId]/route.get.ts");
|
|
151
|
-
assert_eq!(route.regex, r"^\/users\/([^\/]+)\/posts\/([^\/]+)$");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
#[test]
|
|
155
|
-
fn processes_index_routes_as_regular_routes() {
|
|
156
|
-
let p = processor();
|
|
157
|
-
|
|
158
|
-
// Root "index" is just "index" now
|
|
159
|
-
let route = p.process_route_file(&file_info("index/route.ts", "/project/src/index/route.ts"));
|
|
160
|
-
assert_eq!(route.path, "/index");
|
|
161
|
-
assert_eq!(route.method, None);
|
|
162
|
-
assert!(!route.dynamic);
|
|
163
|
-
|
|
164
|
-
// Nested index
|
|
165
|
-
let route = p.process_route_file(&file_info("users/index/route.ts", "/project/src/users/index/route.ts"));
|
|
166
|
-
assert_eq!(route.path, "/users/index");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
#[test]
|
|
170
|
-
fn processes_route_groups() {
|
|
171
|
-
let p = processor();
|
|
172
|
-
|
|
173
|
-
// Simple group
|
|
174
|
-
let route = p.process_route_file(&file_info("(v1)/users/route.ts", "/project/src/(v1)/users/route.ts"));
|
|
175
|
-
assert_eq!(route.path, "/users");
|
|
176
|
-
assert!(!route.dynamic);
|
|
177
|
-
assert_eq!(route.file_path, "/project/src/(v1)/users/route.ts");
|
|
178
|
-
|
|
179
|
-
// Group with dynamic route and method
|
|
180
|
-
let route = p.process_route_file(&file_info(
|
|
181
|
-
"(api)/users/[id]/route.delete.ts",
|
|
182
|
-
"/project/src/(api)/users/[id]/route.delete.ts",
|
|
183
|
-
));
|
|
184
|
-
assert_eq!(route.path, "/users/:id");
|
|
185
|
-
assert_eq!(route.method, Some(MatchedMethodSuffix::Delete));
|
|
186
|
-
assert!(route.dynamic);
|
|
187
|
-
assert_eq!(route.file_path, "/project/src/(api)/users/[id]/route.delete.ts");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
#[test]
|
|
191
|
-
fn handles_windows_paths() {
|
|
192
|
-
let p = processor();
|
|
193
|
-
let route = p.process_route_file(&file_info(
|
|
194
|
-
r"users\[id]\route.ts",
|
|
195
|
-
r"C:\project\src\users\[id]\route.ts",
|
|
196
|
-
));
|
|
197
|
-
assert_eq!(route.path, "/users/:id");
|
|
198
|
-
assert!(route.dynamic);
|
|
199
|
-
assert_eq!(route.file_path, r"C:\project\src\users\[id]\route.ts");
|
|
200
|
-
}
|
|
201
|
-
}
|