titanpl 1.0.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.
- package/LICENSE +15 -0
- package/README.md +254 -0
- package/index.js +24 -0
- package/package.json +83 -0
- package/packages/cli/index.js +141 -0
- package/templates/common/.env +1 -0
- package/templates/common/Dockerfile +66 -0
- package/templates/common/_dockerignore +35 -0
- package/templates/common/_gitignore +33 -0
- package/templates/common/app/t.native.d.ts +2043 -0
- package/templates/common/app/t.native.js +39 -0
- package/templates/extension/README.md +65 -0
- package/templates/extension/index.d.ts +27 -0
- package/templates/extension/index.js +28 -0
- package/templates/extension/jsconfig.json +14 -0
- package/templates/extension/native/Cargo.toml +9 -0
- package/templates/extension/native/src/lib.rs +5 -0
- package/templates/extension/package-lock.json +522 -0
- package/templates/extension/package.json +26 -0
- package/templates/extension/titan.json +18 -0
- package/templates/js/app/actions/getuser.js +9 -0
- package/templates/js/app/app.js +7 -0
- package/templates/js/eslint.config.js +5 -0
- package/templates/js/jsconfig.json +27 -0
- package/templates/js/package.json +27 -0
- package/templates/rust-js/app/actions/getuser.js +9 -0
- package/templates/rust-js/app/actions/rust_hello.rs +14 -0
- package/templates/rust-js/app/app.js +9 -0
- package/templates/rust-js/eslint.config.js +5 -0
- package/templates/rust-js/jsconfig.json +27 -0
- package/templates/rust-js/package.json +27 -0
- package/templates/rust-js/titan/bundle.js +157 -0
- package/templates/rust-js/titan/dev.js +323 -0
- package/templates/rust-js/titan/titan.js +126 -0
- package/templates/rust-ts/app/actions/getuser.ts +9 -0
- package/templates/rust-ts/app/actions/rust_hello.rs +14 -0
- package/templates/rust-ts/app/app.ts +9 -0
- package/templates/rust-ts/eslint.config.js +12 -0
- package/templates/rust-ts/package.json +29 -0
- package/templates/rust-ts/titan/bundle.js +163 -0
- package/templates/rust-ts/titan/dev.js +435 -0
- package/templates/rust-ts/titan/titan.d.ts +19 -0
- package/templates/rust-ts/titan/titan.js +124 -0
- package/templates/rust-ts/tsconfig.json +28 -0
- package/templates/ts/app/actions/getuser.ts +9 -0
- package/templates/ts/app/app.ts +7 -0
- package/templates/ts/eslint.config.js +12 -0
- package/templates/ts/package.json +29 -0
- package/templates/ts/tsconfig.json +28 -0
- package/titanpl-sdk/LICENSE +15 -0
- package/titanpl-sdk/README.md +109 -0
- package/titanpl-sdk/assets/titanpl-sdk.png +0 -0
- package/titanpl-sdk/bin/run.js +274 -0
- package/titanpl-sdk/index.js +5 -0
- package/titanpl-sdk/package-lock.json +28 -0
- package/titanpl-sdk/package.json +40 -0
- package/titanpl-sdk/templates/app/actions/hello.js +5 -0
- package/titanpl-sdk/templates/app/app.js +7 -0
- package/titanpl-sdk/templates/jsconfig.json +19 -0
- package/titanpl-sdk/templates/server/Cargo.toml +52 -0
- package/titanpl-sdk/templates/server/src/action_management.rs +175 -0
- package/titanpl-sdk/templates/server/src/errors.rs +12 -0
- package/titanpl-sdk/templates/server/src/extensions/builtin.rs +1038 -0
- package/titanpl-sdk/templates/server/src/extensions/external.rs +338 -0
- package/titanpl-sdk/templates/server/src/extensions/mod.rs +580 -0
- package/titanpl-sdk/templates/server/src/extensions/titan_core.js +249 -0
- package/titanpl-sdk/templates/server/src/fast_path.rs +719 -0
- package/titanpl-sdk/templates/server/src/main.rs +607 -0
- package/titanpl-sdk/templates/server/src/runtime.rs +284 -0
- package/titanpl-sdk/templates/server/src/utils.rs +33 -0
- package/titanpl-sdk/templates/titan/bundle.js +259 -0
- package/titanpl-sdk/templates/titan/dev.js +390 -0
- package/titanpl-sdk/templates/titan/error-box.js +277 -0
- package/titanpl-sdk/templates/titan/titan.js +129 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
//! Worker Pool Management (Performance Optimized)
|
|
2
|
+
//!
|
|
3
|
+
//! Features:
|
|
4
|
+
//! 1. Work-stealing fallback strategy.
|
|
5
|
+
//! 2. Bounded channel capacity for pipeline handling.
|
|
6
|
+
//! 3. Batch-ready architecture for HTTP pipelining.
|
|
7
|
+
//! 4. Zero-copy / deferred cloning where possible.
|
|
8
|
+
|
|
9
|
+
use bytes::Bytes;
|
|
10
|
+
use crossbeam::channel::{bounded, Sender, TrySendError};
|
|
11
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
12
|
+
use std::thread;
|
|
13
|
+
use tokio::sync::mpsc;
|
|
14
|
+
use tokio::sync::oneshot;
|
|
15
|
+
use smallvec::SmallVec;
|
|
16
|
+
|
|
17
|
+
use crate::extensions::{self, AsyncOpRequest, TitanRuntime, WorkerAsyncResult};
|
|
18
|
+
|
|
19
|
+
pub struct RuntimeManager {
|
|
20
|
+
request_txs: Vec<Sender<WorkerCommand>>,
|
|
21
|
+
round_robin_counter: AtomicUsize,
|
|
22
|
+
num_workers: usize,
|
|
23
|
+
_workers: Vec<thread::JoinHandle<()>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub enum WorkerCommand {
|
|
27
|
+
Request(RequestTask),
|
|
28
|
+
Resume {
|
|
29
|
+
drift_id: u32,
|
|
30
|
+
result: WorkerAsyncResult,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[allow(dead_code)]
|
|
35
|
+
pub struct RequestTask {
|
|
36
|
+
pub action_name: String,
|
|
37
|
+
pub body: Option<Bytes>,
|
|
38
|
+
pub method: String,
|
|
39
|
+
pub path: String,
|
|
40
|
+
pub headers: SmallVec<[(String, String); 8]>,
|
|
41
|
+
pub params: SmallVec<[(String, String); 4]>,
|
|
42
|
+
pub query: SmallVec<[(String, String); 4]>,
|
|
43
|
+
pub response_tx: oneshot::Sender<WorkerResult>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub struct WorkerResult {
|
|
47
|
+
pub json: serde_json::Value,
|
|
48
|
+
pub timings: Vec<(String, f64)>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl RuntimeManager {
|
|
52
|
+
pub fn new(
|
|
53
|
+
project_root: std::path::PathBuf,
|
|
54
|
+
num_threads: usize,
|
|
55
|
+
stack_size: usize,
|
|
56
|
+
) -> Self {
|
|
57
|
+
let (async_tx, mut async_rx) = mpsc::channel::<AsyncOpRequest>(2048);
|
|
58
|
+
let tokio_handle = tokio::runtime::Handle::current();
|
|
59
|
+
|
|
60
|
+
// Spawn Tokio Async Handler (for drift operations)
|
|
61
|
+
tokio_handle.spawn(async move {
|
|
62
|
+
while let Some(req) = async_rx.recv().await {
|
|
63
|
+
let drift_id = req.drift_id;
|
|
64
|
+
let respond_tx = req.respond_tx;
|
|
65
|
+
tokio::spawn(async move {
|
|
66
|
+
let start = std::time::Instant::now();
|
|
67
|
+
let result = extensions::builtin::run_async_operation(req.op).await;
|
|
68
|
+
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
|
|
69
|
+
let _ = respond_tx.send(WorkerAsyncResult {
|
|
70
|
+
drift_id,
|
|
71
|
+
result,
|
|
72
|
+
duration_ms,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Create worker channels
|
|
79
|
+
let channel_capacity = 256;
|
|
80
|
+
let mut workers = Vec::with_capacity(num_threads);
|
|
81
|
+
|
|
82
|
+
let mut channels: Vec<(Sender<WorkerCommand>, crossbeam::channel::Receiver<WorkerCommand>)> =
|
|
83
|
+
Vec::with_capacity(num_threads);
|
|
84
|
+
|
|
85
|
+
for _ in 0..num_threads {
|
|
86
|
+
let (tx, rx) = bounded(channel_capacity);
|
|
87
|
+
channels.push((tx, rx));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let mut final_txs: Vec<Sender<WorkerCommand>> = Vec::with_capacity(num_threads);
|
|
91
|
+
for (tx, _) in &channels {
|
|
92
|
+
final_txs.push(tx.clone());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Spawn Worker Threads
|
|
96
|
+
for (i, (tx, rx)) in channels.into_iter().enumerate() {
|
|
97
|
+
let my_tx = tx.clone();
|
|
98
|
+
let root = project_root.clone();
|
|
99
|
+
let handle = tokio_handle.clone();
|
|
100
|
+
let async_tx = async_tx.clone();
|
|
101
|
+
|
|
102
|
+
let handle = thread::Builder::new()
|
|
103
|
+
.name(format!("titan-worker-{}", i))
|
|
104
|
+
.stack_size(stack_size)
|
|
105
|
+
.spawn(move || {
|
|
106
|
+
let mut rt = extensions::init_runtime_worker(
|
|
107
|
+
i,
|
|
108
|
+
root,
|
|
109
|
+
my_tx,
|
|
110
|
+
handle,
|
|
111
|
+
async_tx,
|
|
112
|
+
stack_size,
|
|
113
|
+
);
|
|
114
|
+
rt.bind_to_isolate();
|
|
115
|
+
|
|
116
|
+
loop {
|
|
117
|
+
match rx.recv() {
|
|
118
|
+
Ok(cmd) => match cmd {
|
|
119
|
+
WorkerCommand::Request(task) => {
|
|
120
|
+
handle_new_request(task, &mut rt);
|
|
121
|
+
}
|
|
122
|
+
WorkerCommand::Resume { drift_id, result } => {
|
|
123
|
+
handle_resume(drift_id, result, &mut rt);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
Err(_) => break,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.expect("Failed to spawn worker");
|
|
131
|
+
|
|
132
|
+
workers.push(handle);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Self {
|
|
136
|
+
request_txs: final_txs,
|
|
137
|
+
round_robin_counter: AtomicUsize::new(0),
|
|
138
|
+
num_workers: num_threads,
|
|
139
|
+
_workers: workers,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Execute an action on a worker. Uses round-robin with work-stealing fallback.
|
|
144
|
+
pub async fn execute(
|
|
145
|
+
&self,
|
|
146
|
+
action: String,
|
|
147
|
+
method: String,
|
|
148
|
+
path: String,
|
|
149
|
+
body: Option<Bytes>,
|
|
150
|
+
headers: SmallVec<[(String, String); 8]>,
|
|
151
|
+
params: SmallVec<[(String, String); 4]>,
|
|
152
|
+
query: SmallVec<[(String, String); 4]>,
|
|
153
|
+
) -> Result<(serde_json::Value, Vec<(String, f64)>), String> {
|
|
154
|
+
let (tx, rx) = oneshot::channel();
|
|
155
|
+
let task = RequestTask {
|
|
156
|
+
action_name: action,
|
|
157
|
+
body,
|
|
158
|
+
method,
|
|
159
|
+
path,
|
|
160
|
+
headers,
|
|
161
|
+
params,
|
|
162
|
+
query,
|
|
163
|
+
response_tx: tx,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Work-Stealing Distribution
|
|
167
|
+
let start_idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % self.num_workers;
|
|
168
|
+
let mut cmd = WorkerCommand::Request(task);
|
|
169
|
+
|
|
170
|
+
for attempt in 0..self.num_workers {
|
|
171
|
+
let idx = (start_idx + attempt) % self.num_workers;
|
|
172
|
+
match self.request_txs[idx].try_send(cmd) {
|
|
173
|
+
Ok(()) => {
|
|
174
|
+
return match rx.await {
|
|
175
|
+
Ok(res) => Ok((res.json, res.timings)),
|
|
176
|
+
Err(_) => Err("Worker channel closed".to_string()),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
Err(TrySendError::Full(returned)) => {
|
|
180
|
+
cmd = returned;
|
|
181
|
+
}
|
|
182
|
+
Err(TrySendError::Disconnected(_)) => {
|
|
183
|
+
return Err("Worker disconnected".to_string());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// All workers full — blocking send to the original target as last resort
|
|
189
|
+
self.request_txs[start_idx]
|
|
190
|
+
.send(cmd)
|
|
191
|
+
.map_err(|e| e.to_string())?;
|
|
192
|
+
|
|
193
|
+
match rx.await {
|
|
194
|
+
Ok(res) => Ok((res.json, res.timings)),
|
|
195
|
+
Err(_) => Err("Worker channel closed".to_string()),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Handle a new incoming request.
|
|
201
|
+
///
|
|
202
|
+
/// OPTIMIZATION: Deferred cloning.
|
|
203
|
+
/// Only stores data if drift (async suspend) happens.
|
|
204
|
+
fn handle_new_request(task: RequestTask, rt: &mut TitanRuntime) {
|
|
205
|
+
rt.request_counter += 1;
|
|
206
|
+
let request_id = rt.request_counter;
|
|
207
|
+
|
|
208
|
+
// Move response_tx into pending (partial move of task — other fields remain accessible)
|
|
209
|
+
rt.pending_requests.insert(request_id, task.response_tx);
|
|
210
|
+
|
|
211
|
+
let drift_count = rt.drift_counter;
|
|
212
|
+
rt.request_start_counters.insert(request_id, drift_count);
|
|
213
|
+
|
|
214
|
+
// Execute action — pass references, body is O(1) Bytes clone
|
|
215
|
+
extensions::execute_action_optimized(
|
|
216
|
+
rt,
|
|
217
|
+
request_id,
|
|
218
|
+
&task.action_name,
|
|
219
|
+
task.body.clone(), // Bytes::clone() is O(1) refcount bump
|
|
220
|
+
&task.method,
|
|
221
|
+
&task.path,
|
|
222
|
+
&task.headers,
|
|
223
|
+
&task.params,
|
|
224
|
+
&task.query,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Deferred cloning decision
|
|
228
|
+
if !rt.pending_requests.contains_key(&request_id) {
|
|
229
|
+
// Completed synchronously — no data needed, minimal cleanup
|
|
230
|
+
rt.request_start_counters.remove(&request_id);
|
|
231
|
+
} else {
|
|
232
|
+
// Suspended via drift — MOVE (not clone) data for resume replay.
|
|
233
|
+
rt.active_requests.insert(
|
|
234
|
+
request_id,
|
|
235
|
+
extensions::RequestData {
|
|
236
|
+
action_name: task.action_name,
|
|
237
|
+
body: task.body,
|
|
238
|
+
method: task.method,
|
|
239
|
+
path: task.path,
|
|
240
|
+
headers: task.headers.into_vec(),
|
|
241
|
+
params: task.params.into_vec(),
|
|
242
|
+
query: task.query.into_vec(),
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn handle_resume(drift_id: u32, result: WorkerAsyncResult, rt: &mut TitanRuntime) {
|
|
249
|
+
let req_id = rt.drift_to_request.get(&drift_id).copied().unwrap_or(0);
|
|
250
|
+
|
|
251
|
+
let timing_type = if result.result.get("error").is_some() {
|
|
252
|
+
"drift_error"
|
|
253
|
+
} else {
|
|
254
|
+
"drift"
|
|
255
|
+
};
|
|
256
|
+
rt.request_timings
|
|
257
|
+
.entry(req_id)
|
|
258
|
+
.or_default()
|
|
259
|
+
.push((timing_type.to_string(), result.duration_ms));
|
|
260
|
+
|
|
261
|
+
rt.completed_drifts.insert(drift_id, result.result);
|
|
262
|
+
|
|
263
|
+
if let Some(req_data) = rt.active_requests.get(&req_id).cloned() {
|
|
264
|
+
let start_counter = rt.request_start_counters.get(&req_id).copied().unwrap_or(0);
|
|
265
|
+
rt.drift_counter = start_counter;
|
|
266
|
+
|
|
267
|
+
extensions::execute_action_optimized(
|
|
268
|
+
rt,
|
|
269
|
+
req_id,
|
|
270
|
+
&req_data.action_name,
|
|
271
|
+
req_data.body,
|
|
272
|
+
&req_data.method,
|
|
273
|
+
&req_data.path,
|
|
274
|
+
&req_data.headers,
|
|
275
|
+
&req_data.params,
|
|
276
|
+
&req_data.query,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if req_id != 0 && !rt.pending_requests.contains_key(&req_id) {
|
|
281
|
+
rt.active_requests.remove(&req_id);
|
|
282
|
+
rt.request_start_counters.remove(&req_id);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//! Terminal styling and utility functions.
|
|
2
|
+
|
|
3
|
+
pub fn blue(s: &str) -> String {
|
|
4
|
+
format!("\x1b[38;5;39m{}\x1b[0m", s)
|
|
5
|
+
}
|
|
6
|
+
pub fn white(s: &str) -> String {
|
|
7
|
+
format!("\x1b[39m{}\x1b[0m", s)
|
|
8
|
+
}
|
|
9
|
+
pub fn yellow(s: &str) -> String {
|
|
10
|
+
format!("\x1b[33m{}\x1b[0m", s)
|
|
11
|
+
}
|
|
12
|
+
pub fn green(s: &str) -> String {
|
|
13
|
+
format!("\x1b[32m{}\x1b[0m", s)
|
|
14
|
+
}
|
|
15
|
+
pub fn gray(s: &str) -> String {
|
|
16
|
+
format!("\x1b[90m{}\x1b[0m", s)
|
|
17
|
+
}
|
|
18
|
+
pub fn red(s: &str) -> String {
|
|
19
|
+
format!("\x1b[31m{}\x1b[0m", s)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn parse_expires_in(value: &str) -> Option<u64> {
|
|
23
|
+
let (num, unit) = value.split_at(value.len() - 1);
|
|
24
|
+
let n: u64 = num.parse().ok()?;
|
|
25
|
+
|
|
26
|
+
match unit {
|
|
27
|
+
"s" => Some(n),
|
|
28
|
+
"m" => Some(n * 60),
|
|
29
|
+
"h" => Some(n * 60 * 60),
|
|
30
|
+
"d" => Some(n * 60 * 60 * 24),
|
|
31
|
+
_ => None,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle.js
|
|
3
|
+
* Handles esbuild bundling with comprehensive error reporting
|
|
4
|
+
* RULE: This file handles ALL esbuild errors and prints error boxes directly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import esbuild from 'esbuild';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import { renderErrorBox, parseEsbuildError } from './error-box.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Required for resolving node_modules inside ESM
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Titan Node Builtin Rewrite Map
|
|
22
|
+
* Rewrites Node builtins to @titanpl/node shims
|
|
23
|
+
*/
|
|
24
|
+
const NODE_BUILTIN_MAP = {
|
|
25
|
+
"fs": "@titanpl/node/fs",
|
|
26
|
+
"node:fs": "@titanpl/node/fs",
|
|
27
|
+
|
|
28
|
+
"path": "@titanpl/node/path",
|
|
29
|
+
"node:path": "@titanpl/node/path",
|
|
30
|
+
|
|
31
|
+
"os": "@titanpl/node/os",
|
|
32
|
+
"node:os": "@titanpl/node/os",
|
|
33
|
+
|
|
34
|
+
"crypto": "@titanpl/node/crypto",
|
|
35
|
+
"node:crypto": "@titanpl/node/crypto",
|
|
36
|
+
|
|
37
|
+
"process": "@titanpl/node/process",
|
|
38
|
+
|
|
39
|
+
"util": "@titanpl/node/util",
|
|
40
|
+
"node:util": "@titanpl/node/util",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Titan Node Compatibility Plugin
|
|
45
|
+
* Rewrites require/import of Node builtins
|
|
46
|
+
* Returns absolute paths (required by esbuild)
|
|
47
|
+
*/
|
|
48
|
+
const titanNodeCompatPlugin = {
|
|
49
|
+
name: "titan-node-compat",
|
|
50
|
+
setup(build) {
|
|
51
|
+
build.onResolve({ filter: /.*/ }, args => {
|
|
52
|
+
if (NODE_BUILTIN_MAP[args.path]) {
|
|
53
|
+
try {
|
|
54
|
+
const resolved = require.resolve(NODE_BUILTIN_MAP[args.path]);
|
|
55
|
+
return { path: resolved };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[Titan] Failed to resolve Node shim: ${NODE_BUILTIN_MAP[args.path]}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get Titan version for error branding
|
|
68
|
+
*/
|
|
69
|
+
function getTitanVersion() {
|
|
70
|
+
try {
|
|
71
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
72
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return "0.1.0";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Custom error class for bundle errors
|
|
80
|
+
*/
|
|
81
|
+
export class BundleError extends Error {
|
|
82
|
+
constructor(message, errors = [], warnings = []) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = 'BundleError';
|
|
85
|
+
this.errors = errors;
|
|
86
|
+
this.warnings = warnings;
|
|
87
|
+
this.isBundleError = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate entry file exists
|
|
93
|
+
*/
|
|
94
|
+
async function validateEntryPoint(entryPoint) {
|
|
95
|
+
const absPath = path.resolve(entryPoint);
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(absPath)) {
|
|
98
|
+
throw new BundleError(
|
|
99
|
+
`Entry point does not exist: ${entryPoint}`,
|
|
100
|
+
[{ text: `Cannot find file: ${absPath}`, location: { file: entryPoint } }]
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await fs.promises.access(absPath, fs.constants.R_OK);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new BundleError(
|
|
108
|
+
`Entry point is not readable: ${entryPoint}`,
|
|
109
|
+
[{ text: `Cannot read file: ${absPath}`, location: { file: entryPoint } }]
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Bundles a single file
|
|
116
|
+
*/
|
|
117
|
+
export async function bundleFile(options) {
|
|
118
|
+
const {
|
|
119
|
+
entryPoint,
|
|
120
|
+
outfile,
|
|
121
|
+
format = 'iife',
|
|
122
|
+
minify = false,
|
|
123
|
+
sourcemap = false,
|
|
124
|
+
platform = 'neutral',
|
|
125
|
+
globalName = '__titan_exports',
|
|
126
|
+
target = 'es2020',
|
|
127
|
+
banner = {},
|
|
128
|
+
footer = {}
|
|
129
|
+
} = options;
|
|
130
|
+
|
|
131
|
+
await validateEntryPoint(entryPoint);
|
|
132
|
+
|
|
133
|
+
const outDir = path.dirname(outfile);
|
|
134
|
+
await fs.promises.mkdir(outDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = await esbuild.build({
|
|
138
|
+
entryPoints: [entryPoint],
|
|
139
|
+
bundle: true,
|
|
140
|
+
outfile,
|
|
141
|
+
format,
|
|
142
|
+
globalName,
|
|
143
|
+
platform,
|
|
144
|
+
target,
|
|
145
|
+
banner,
|
|
146
|
+
footer,
|
|
147
|
+
minify,
|
|
148
|
+
sourcemap,
|
|
149
|
+
logLevel: 'silent',
|
|
150
|
+
logLimit: 0,
|
|
151
|
+
write: true,
|
|
152
|
+
metafile: false,
|
|
153
|
+
plugins: [titanNodeCompatPlugin],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (result.errors?.length) {
|
|
157
|
+
throw new BundleError(
|
|
158
|
+
`Build failed with ${result.errors.length} error(s)`,
|
|
159
|
+
result.errors,
|
|
160
|
+
result.warnings || []
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.errors?.length) {
|
|
166
|
+
throw new BundleError(
|
|
167
|
+
`Build failed with ${err.errors.length} error(s)`,
|
|
168
|
+
err.errors,
|
|
169
|
+
err.warnings || []
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new BundleError(
|
|
174
|
+
`Unexpected build error: ${err.message}`,
|
|
175
|
+
[{ text: err.message, location: { file: entryPoint } }]
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Main bundler
|
|
182
|
+
*/
|
|
183
|
+
export async function bundle() {
|
|
184
|
+
const root = process.cwd();
|
|
185
|
+
const actionsDir = path.join(root, 'app', 'actions');
|
|
186
|
+
const bundleDir = path.join(root, 'server', 'src', 'actions');
|
|
187
|
+
|
|
188
|
+
if (fs.existsSync(bundleDir)) {
|
|
189
|
+
fs.rmSync(bundleDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
await fs.promises.mkdir(bundleDir, { recursive: true });
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(actionsDir)) return;
|
|
194
|
+
|
|
195
|
+
const files = fs.readdirSync(actionsDir).filter(f =>
|
|
196
|
+
(f.endsWith('.js') || f.endsWith('.ts')) && !f.endsWith('.d.ts')
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
const actionName = path.basename(file, path.extname(file));
|
|
201
|
+
const entryPoint = path.join(actionsDir, file);
|
|
202
|
+
const outfile = path.join(bundleDir, actionName + ".jsbundle");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await bundleFile({
|
|
206
|
+
entryPoint,
|
|
207
|
+
outfile,
|
|
208
|
+
format: 'iife',
|
|
209
|
+
globalName: '__titan_exports',
|
|
210
|
+
platform: 'node',
|
|
211
|
+
target: 'es2020',
|
|
212
|
+
banner: { js: "var Titan = t;" },
|
|
213
|
+
footer: {
|
|
214
|
+
js: `
|
|
215
|
+
(function () {
|
|
216
|
+
const fn =
|
|
217
|
+
__titan_exports["${actionName}"] ||
|
|
218
|
+
__titan_exports.default;
|
|
219
|
+
|
|
220
|
+
if (typeof fn !== "function") {
|
|
221
|
+
throw new Error("[Titan] Action '${actionName}' not found or not a function");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
globalThis["${actionName}"] = globalThis.defineAction(fn);
|
|
225
|
+
})();
|
|
226
|
+
`
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
|
|
232
|
+
console.error();
|
|
233
|
+
|
|
234
|
+
const titanVersion = getTitanVersion();
|
|
235
|
+
|
|
236
|
+
if (error.isBundleError && error.errors?.length) {
|
|
237
|
+
for (let i = 0; i < error.errors.length; i++) {
|
|
238
|
+
const errorInfo = parseEsbuildError(error.errors[i]);
|
|
239
|
+
if (error.errors.length > 1) {
|
|
240
|
+
errorInfo.title = `Build Error ${i + 1}/${error.errors.length}`;
|
|
241
|
+
}
|
|
242
|
+
errorInfo.titanVersion = titanVersion;
|
|
243
|
+
console.error(renderErrorBox(errorInfo));
|
|
244
|
+
console.error();
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
const errorInfo = {
|
|
248
|
+
title: 'Build Error',
|
|
249
|
+
file: entryPoint,
|
|
250
|
+
message: error.message || 'Unknown error',
|
|
251
|
+
titanVersion
|
|
252
|
+
};
|
|
253
|
+
console.error(renderErrorBox(errorInfo));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new Error('__TITAN_BUNDLE_FAILED__');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|