@ezetgalaxy/titan 26.14.1 → 26.15.0

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/templates/js/server/Cargo.toml +1 -1
  3. package/templates/js/server/src/action_management.rs +175 -159
  4. package/templates/js/server/src/errors.rs +12 -10
  5. package/templates/js/server/src/extensions/builtin.rs +928 -778
  6. package/templates/js/server/src/extensions/external.rs +9 -0
  7. package/templates/js/server/src/extensions/mod.rs +580 -522
  8. package/templates/js/server/src/extensions/titan_core.js +13 -0
  9. package/templates/js/server/src/fast_path.rs +719 -654
  10. package/templates/js/server/src/main.rs +607 -555
  11. package/templates/js/server/src/runtime.rs +284 -262
  12. package/templates/js/server/src/utils.rs +33 -32
  13. package/templates/ts/server/Cargo.toml +1 -1
  14. package/templates/ts/server/src/action_management.rs +175 -159
  15. package/templates/ts/server/src/errors.rs +12 -10
  16. package/templates/ts/server/src/extensions/builtin.rs +928 -778
  17. package/templates/ts/server/src/extensions/external.rs +9 -0
  18. package/templates/ts/server/src/extensions/mod.rs +580 -522
  19. package/templates/ts/server/src/extensions/titan_core.js +13 -0
  20. package/templates/ts/server/src/fast_path.rs +719 -654
  21. package/templates/ts/server/src/main.rs +607 -555
  22. package/templates/ts/server/src/runtime.rs +284 -262
  23. package/templates/ts/server/src/utils.rs +33 -32
  24. package/titanpl-sdk/package.json +1 -1
  25. package/titanpl-sdk/templates/server/Cargo.toml +1 -1
  26. package/titanpl-sdk/templates/server/src/action_management.rs +175 -159
  27. package/titanpl-sdk/templates/server/src/errors.rs +12 -10
  28. package/titanpl-sdk/templates/server/src/extensions/builtin.rs +928 -778
  29. package/titanpl-sdk/templates/server/src/extensions/external.rs +9 -0
  30. package/titanpl-sdk/templates/server/src/extensions/mod.rs +580 -522
  31. package/titanpl-sdk/templates/server/src/extensions/titan_core.js +13 -0
  32. package/titanpl-sdk/templates/server/src/fast_path.rs +719 -654
  33. package/titanpl-sdk/templates/server/src/main.rs +607 -555
  34. package/titanpl-sdk/templates/server/src/runtime.rs +284 -262
  35. package/titanpl-sdk/templates/server/src/utils.rs +33 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "26.14.1",
3
+ "version": "26.15.0",
4
4
  "description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -10,7 +10,7 @@ reqwest = { version = "0.12.24", features = ["json", "rustls-tls", "gzip", "brot
10
10
  serde = { version = "1.0.228", features = ["derive"] }
11
11
  serde_json = "1.0.145"
12
12
  thiserror = "2.0.17"
13
- tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "process"] }
13
+ tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "process", "fs"] }
14
14
  tower-http = { version = "0.6.7", features = ["cors"] }
15
15
  tracing = "0.1.43"
16
16
  tracing-subscriber = "0.3.22"
@@ -1,159 +1,175 @@
1
-
2
- use std::collections::HashMap;
3
- use std::env;
4
- use std::path::{Path, PathBuf};
5
- use serde::Deserialize;
6
- use serde_json::Value;
7
-
8
- #[derive(Debug, Deserialize, Clone)]
9
- pub struct RouteVal {
10
- pub r#type: String,
11
- #[serde(alias = "target")]
12
- pub value: Value,
13
- }
14
-
15
- #[derive(Debug, Deserialize, Clone)]
16
- pub struct DynamicRoute {
17
- pub method: String,
18
- pub pattern: String,
19
- pub action: String,
20
- }
21
-
22
- pub fn resolve_actions_dir() -> PathBuf {
23
- if let Ok(override_dir) = env::var("TITAN_ACTIONS_DIR") {
24
- return PathBuf::from(override_dir);
25
- }
26
-
27
- if Path::new("/app/actions").exists() {
28
- return PathBuf::from("/app/actions");
29
- }
30
-
31
- if let Ok(exe) = std::env::current_exe() {
32
- if let Some(parent) = exe.parent() {
33
- if let Some(target_dir) = parent.parent() {
34
- if let Some(server_dir) = target_dir.parent() {
35
- let candidate = server_dir.join("src").join("actions");
36
- if candidate.exists() {
37
- return candidate;
38
- }
39
- let candidate2 = server_dir.join("actions");
40
- if candidate2.exists() {
41
- return candidate2;
42
- }
43
- }
44
- }
45
- }
46
- }
47
-
48
- PathBuf::from("./src/actions")
49
- }
50
-
51
- pub fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
52
- let candidates = [
53
- project_root.join("server").join("src").join("actions"),
54
- project_root.join("server").join("actions"),
55
- project_root.join("app").join("actions"),
56
- project_root.join("actions"),
57
-
58
- project_root.join("..").join("server").join("actions"),
59
- PathBuf::from("/app").join("actions"),
60
- PathBuf::from("actions"),
61
- ];
62
-
63
- for p in &candidates {
64
- if p.exists() && p.is_dir() {
65
- return Some(p.clone());
66
- }
67
- }
68
-
69
- None
70
- }
71
-
72
- pub fn match_dynamic_route(
73
- method: &str,
74
- path: &str,
75
- routes: &[DynamicRoute],
76
- ) -> Option<(String, HashMap<String, String>)> {
77
- let path_segments: Vec<&str> =
78
- path.trim_matches('/').split('/').collect();
79
-
80
- for route in routes {
81
- if route.method != method {
82
- continue;
83
- }
84
-
85
- let pattern_segments: Vec<&str> =
86
- route.pattern.trim_matches('/').split('/').collect();
87
-
88
- if pattern_segments.len() != path_segments.len() {
89
- continue;
90
- }
91
-
92
- let mut params = HashMap::new();
93
- let mut matched = true;
94
-
95
- for (pat, val) in pattern_segments.iter().zip(path_segments.iter()) {
96
- if pat.starts_with(':') {
97
- let inner = &pat[1..];
98
-
99
- let (name, ty) = inner
100
- .split_once('<')
101
- .map(|(n, t)| (n, t.trim_end_matches('>')))
102
- .unwrap_or((inner, "string"));
103
-
104
- let valid = match ty {
105
- "number" => val.parse::<i64>().is_ok(),
106
- "string" => true,
107
- _ => false,
108
- };
109
-
110
- if !valid {
111
- matched = false;
112
- break;
113
- }
114
-
115
- params.insert(name.to_string(), (*val).to_string());
116
- } else if pat != val {
117
- matched = false;
118
- break;
119
- }
120
- }
121
-
122
- if matched {
123
- return Some((route.action.clone(), params));
124
- }
125
- }
126
-
127
- None
128
- }
129
-
130
- pub fn scan_actions(root: &PathBuf) -> HashMap<String, PathBuf> {
131
- let mut map = HashMap::new();
132
-
133
- let dir = match find_actions_dir(root) {
134
- Some(d) => d,
135
- None => {
136
- let ad = resolve_actions_dir();
137
- if ad.exists() { ad } else { return map; }
138
- }
139
- };
140
-
141
- if let Ok(entries) = std::fs::read_dir(dir) {
142
- for entry in entries.flatten() {
143
- let path = entry.path();
144
- if !path.is_file() { continue; }
145
-
146
- let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
147
- if ext != "js" && ext != "jsbundle" {
148
- continue;
149
- }
150
-
151
- let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
152
- if file_stem.is_empty() { continue; }
153
-
154
- map.insert(file_stem.to_string(), path);
155
- }
156
- }
157
-
158
- map
159
- }
1
+ //! Action Management and Dynamic Routing
2
+ //!
3
+ //! Handles resolution of action directories, scanning for available actions,
4
+ //! and matching dynamic routes (e.g. `/users/:id`).
5
+
6
+ use std::collections::HashMap;
7
+ use std::env;
8
+ use std::path::{Path, PathBuf};
9
+ use serde::Deserialize;
10
+ use serde_json::Value;
11
+
12
+ /// Route configuration (loaded from routes.json)
13
+ #[derive(Debug, Deserialize, Clone)]
14
+ pub struct RouteVal {
15
+ pub r#type: String,
16
+ #[serde(alias = "target")]
17
+ pub value: Value,
18
+ }
19
+
20
+ #[derive(Debug, Deserialize, Clone)]
21
+ pub struct DynamicRoute {
22
+ pub method: String,
23
+ pub pattern: String,
24
+ pub action: String,
25
+ }
26
+
27
+ /// Resolve the directory path where actions are stored.
28
+ pub fn resolve_actions_dir() -> PathBuf {
29
+ // Respect explicit override first
30
+ if let Ok(override_dir) = env::var("TITAN_ACTIONS_DIR") {
31
+ return PathBuf::from(override_dir);
32
+ }
33
+
34
+ // Production container layout
35
+ if Path::new("/app/actions").exists() {
36
+ return PathBuf::from("/app/actions");
37
+ }
38
+
39
+ // Try to walk up from the executing binary to discover `<...>/server/src/actions`
40
+ if let Ok(exe) = std::env::current_exe() {
41
+ if let Some(parent) = exe.parent() {
42
+ if let Some(target_dir) = parent.parent() {
43
+ if let Some(server_dir) = target_dir.parent() {
44
+ let candidate = server_dir.join("src").join("actions");
45
+ if candidate.exists() {
46
+ return candidate;
47
+ }
48
+ let candidate2 = server_dir.join("actions");
49
+ if candidate2.exists() {
50
+ return candidate2;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // Fall back to local ./src/actions
58
+ PathBuf::from("./src/actions")
59
+ }
60
+
61
+ /// Try to find the directory that contains compiled action bundles.
62
+ pub fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
63
+ let candidates = [
64
+ project_root.join("server").join("src").join("actions"),
65
+ project_root.join("server").join("actions"),
66
+ project_root.join("app").join("actions"),
67
+ project_root.join("actions"),
68
+
69
+ project_root.join("..").join("server").join("actions"),
70
+ PathBuf::from("/app").join("actions"),
71
+ PathBuf::from("actions"),
72
+ ];
73
+
74
+ for p in &candidates {
75
+ if p.exists() && p.is_dir() {
76
+ return Some(p.clone());
77
+ }
78
+ }
79
+
80
+ None
81
+ }
82
+
83
+ /// Match a dynamic route against the current request path.
84
+ pub fn match_dynamic_route(
85
+ method: &str,
86
+ path: &str,
87
+ routes: &[DynamicRoute],
88
+ ) -> Option<(String, HashMap<String, String>)> {
89
+ let path_segments: Vec<&str> =
90
+ path.trim_matches('/').split('/').collect();
91
+
92
+ for route in routes {
93
+ if route.method != method {
94
+ continue;
95
+ }
96
+
97
+ let pattern_segments: Vec<&str> =
98
+ route.pattern.trim_matches('/').split('/').collect();
99
+
100
+ if pattern_segments.len() != path_segments.len() {
101
+ continue;
102
+ }
103
+
104
+ let mut params = HashMap::new();
105
+ let mut matched = true;
106
+
107
+ for (pat, val) in pattern_segments.iter().zip(path_segments.iter()) {
108
+ if pat.starts_with(':') {
109
+ let inner = &pat[1..];
110
+
111
+ let (name, ty) = inner
112
+ .split_once('<')
113
+ .map(|(n, t)| (n, t.trim_end_matches('>')))
114
+ .unwrap_or((inner, "string"));
115
+
116
+ let valid = match ty {
117
+ "number" => val.parse::<i64>().is_ok(),
118
+ "string" => true,
119
+ _ => false,
120
+ };
121
+
122
+ if !valid {
123
+ matched = false;
124
+ break;
125
+ }
126
+
127
+ params.insert(name.to_string(), (*val).to_string());
128
+ } else if pat != val {
129
+ matched = false;
130
+ break;
131
+ }
132
+ }
133
+
134
+ if matched {
135
+ return Some((route.action.clone(), params));
136
+ }
137
+ }
138
+
139
+ None
140
+ }
141
+
142
+ /// Scan the resolved actions directory and return a map of action names to file paths.
143
+ pub fn scan_actions(root: &PathBuf) -> HashMap<String, PathBuf> {
144
+ let mut map = HashMap::new();
145
+
146
+ // Locate actions dir - Priority: project root relative paths
147
+ let dir = match find_actions_dir(root) {
148
+ Some(d) => d,
149
+ None => {
150
+ let ad = resolve_actions_dir();
151
+ if ad.exists() { ad } else { return map; }
152
+ }
153
+ };
154
+
155
+ // Scanning actions
156
+ if let Ok(entries) = std::fs::read_dir(dir) {
157
+ for entry in entries.flatten() {
158
+ let path = entry.path();
159
+ if !path.is_file() { continue; }
160
+
161
+ let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
162
+ if ext != "js" && ext != "jsbundle" {
163
+ continue;
164
+ }
165
+
166
+ let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
167
+ if file_stem.is_empty() { continue; }
168
+
169
+ // Found action
170
+ map.insert(file_stem.to_string(), path);
171
+ }
172
+ }
173
+
174
+ map
175
+ }
@@ -1,10 +1,12 @@
1
-
2
- use v8::JsError;
3
-
4
- pub fn format_js_error(err: JsError, action: &str) -> String {
5
- format!(
6
- "Action: {}\n{}",
7
- action,
8
- err.to_string()
9
- )
10
- }
1
+ //! Error handling utilities.
2
+
3
+ use v8::JsError;
4
+
5
+ /// A helper to Format v8 Errors
6
+ pub fn format_js_error(err: JsError, action: &str) -> String {
7
+ format!(
8
+ "Action: {}\n{}",
9
+ action,
10
+ err.to_string()
11
+ )
12
+ }