@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.
@@ -0,0 +1,493 @@
1
+ use std::io::Write;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use swc_atoms::Wtf8Atom;
5
+ use swc_common::{
6
+ comments::SingleThreadedComments,
7
+ errors::{EmitterWriter, Handler},
8
+ source_map::SourceMapGenConfig,
9
+ sync::Lrc,
10
+ BytePos, FileName, Globals, LineCol, Mark, SourceMap, GLOBALS,
11
+ };
12
+ use swc_ecma_codegen::{text_writer::JsWriter, Emitter as CodegenEmitter};
13
+ use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax};
14
+ use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver};
15
+ use swc_ecma_transforms_module::{common_js, path::Resolver as PathResolverEnum};
16
+ use swc_ecma_transforms_typescript::strip;
17
+
18
+ use swc_ecma_ast::{CallExpr, Callee, Expr, ExprOrSpread, Lit, Module, ModuleDecl, ModuleItem};
19
+ use swc_ecma_visit::{VisitMut, VisitMutWith};
20
+
21
+ use crate::builder::sourcemap::write_sourcemap_and_code;
22
+
23
+ use super::tsconfig::TsConfigOptions;
24
+
25
+ /// Simple, thread-safe buffer used to capture diagnostics emitted by SWC.
26
+ ///
27
+ /// The `ErrorBuffer` implements `std::io::Write` and stores emitted bytes
28
+ /// in a shared `Arc<Mutex<Vec<u8>>>`. The native builder uses this buffer to
29
+ /// capture human-readable error output from SWC and return it to the host
30
+ /// instead of writing directly to stderr.
31
+ #[derive(Clone)]
32
+ struct ErrorBuffer {
33
+ buffer: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
34
+ }
35
+
36
+ impl ErrorBuffer {
37
+ fn new() -> Self {
38
+ Self {
39
+ buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
40
+ }
41
+ }
42
+
43
+ fn get_content(&self) -> String {
44
+ self.buffer
45
+ .lock()
46
+ .ok()
47
+ .and_then(|buf| String::from_utf8(buf.clone()).ok())
48
+ .unwrap_or_default()
49
+ }
50
+ }
51
+
52
+ impl Write for ErrorBuffer {
53
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
54
+ self.buffer
55
+ .lock()
56
+ .map_err(|_| std::io::Error::other("Lock failed"))?
57
+ .write(buf)
58
+ }
59
+
60
+ fn flush(&mut self) -> std::io::Result<()> {
61
+ self.buffer
62
+ .lock()
63
+ .map_err(|_| std::io::Error::other("Lock failed"))?
64
+ .flush()
65
+ }
66
+ }
67
+
68
+ /// Small `SourceMapGenConfig` implementation used when emitting source maps.
69
+ ///
70
+ /// It provides basic filename resolution and enables inlining the original
71
+ /// sources content into the generated `.map` file. Keeping the original
72
+ /// content in `sourcesContent` simplifies debugging in the runtime where the
73
+ /// physical source files may not be available.
74
+ struct SourceMapConfigImpl;
75
+
76
+ impl SourceMapGenConfig for SourceMapConfigImpl {
77
+ fn file_name_to_source(&self, f: &FileName) -> String {
78
+ f.to_string()
79
+ }
80
+
81
+ fn inline_sources_content(&self, _: &FileName) -> bool {
82
+ true
83
+ }
84
+ }
85
+
86
+ /// Visitor that rewrites import/require/import() specifiers according to
87
+ /// base_url + paths mapping (simple implementation).
88
+ struct PathsRewriter {
89
+ base_url: PathBuf,
90
+ // Vec of (from_pattern, to_targets)
91
+ paths: Vec<(String, Vec<String>)>,
92
+ // directory of current file being compiled (for generating relative specifiers)
93
+ file_dir: PathBuf,
94
+ }
95
+
96
+ impl PathsRewriter {
97
+ fn resolve_using_paths(&self, spec: &str) -> Option<String> {
98
+ // If spec is relative or absolute, don't touch
99
+ if spec.starts_with('.') || spec.starts_with('/') {
100
+ return None;
101
+ }
102
+
103
+ // Try each mapping
104
+ for (from, to_list) in &self.paths {
105
+ if from.contains('*') {
106
+ // wildcard pattern
107
+ // support exactly one '*' (like the real resolver)
108
+ if from.as_bytes().iter().filter(|&&c| c == b'*').count() != 1 {
109
+ continue;
110
+ }
111
+ let pos = from.find('*').unwrap();
112
+ let prefix = &from[..pos];
113
+ if !spec.starts_with(prefix) {
114
+ continue;
115
+ }
116
+ let extra = &spec[prefix.len()..];
117
+
118
+ for target in to_list {
119
+ let replaced = target.replace('*', extra);
120
+ // Candidate paths to check on disk
121
+ // 1) base_url / replaced
122
+ // 2) base_url / ./replaced
123
+ // Also try common extensions
124
+ if let Some(abs_found) = try_find_file_on_disk(&self.base_url.join(&replaced)) {
125
+ // produce a path relative from file_dir to abs_found
126
+ let rel = make_relative_or_prefixed(&self.file_dir, &abs_found);
127
+ return Some(rel);
128
+ }
129
+
130
+ if let Some(abs_found) =
131
+ try_find_file_on_disk(&self.base_url.join(format!("./{}", replaced)))
132
+ {
133
+ let rel = make_relative_or_prefixed(&self.file_dir, &abs_found);
134
+ return Some(rel);
135
+ }
136
+
137
+ // As fallback, if single target and prefix not empty, mimic SWC's behavior:
138
+ // return "./replaced" even if file not found (matches JS tests expectation).
139
+ if to_list.len() == 1 && !prefix.is_empty() {
140
+ let mut replaced_for_import = replaced.clone();
141
+ if !replaced_for_import.starts_with("./")
142
+ && !replaced_for_import.starts_with('/')
143
+ {
144
+ replaced_for_import = format!("./{}", replaced_for_import);
145
+ }
146
+ return Some(replaced_for_import);
147
+ }
148
+ }
149
+ } else {
150
+ // exact match
151
+ if spec != from {
152
+ continue;
153
+ }
154
+ // to_list must have exactly one entry (TypeScript rule)
155
+ let target = &to_list[0];
156
+ let tp = Path::new(target);
157
+ if tp.is_absolute() {
158
+ if let Some(abs_found) = try_find_file_on_disk(tp) {
159
+ let rel = make_relative_or_prefixed(&self.file_dir, &abs_found);
160
+ return Some(rel);
161
+ }
162
+ // absolute but not found, still return as-is
163
+ return Some(target.clone());
164
+ }
165
+
166
+ // relative to base_url
167
+ if let Some(abs_found) = try_find_file_on_disk(&self.base_url.join(target)) {
168
+ let rel = make_relative_or_prefixed(&self.file_dir, &abs_found);
169
+ return Some(rel);
170
+ }
171
+
172
+ // fallback: return "./target"
173
+ let mut replaced_for_import = target.clone();
174
+ if !replaced_for_import.starts_with("./") && !replaced_for_import.starts_with('/') {
175
+ replaced_for_import = format!("./{}", replaced_for_import);
176
+ }
177
+ return Some(replaced_for_import);
178
+ }
179
+ }
180
+
181
+ // If no mapping matched, try baseUrl + spec (baseUrl resolution)
182
+ let candidate = self.base_url.join(spec);
183
+ if let Some(abs_found) = try_find_file_on_disk(&candidate) {
184
+ let rel = make_relative_or_prefixed(&self.file_dir, &abs_found);
185
+ return Some(rel);
186
+ }
187
+
188
+ None
189
+ }
190
+ }
191
+
192
+ impl VisitMut for PathsRewriter {
193
+ fn visit_mut_module(&mut self, n: &mut Module) {
194
+ // Walk module items and rewrite specifiers where applicable
195
+ n.visit_mut_children_with(self);
196
+ }
197
+
198
+ fn visit_mut_module_item(&mut self, n: &mut ModuleItem) {
199
+ match n {
200
+ ModuleItem::ModuleDecl(decl) => match decl {
201
+ ModuleDecl::Import(import_decl) => {
202
+ let orig = import_decl.src.value.to_string_lossy().to_string();
203
+ if let Some(new_spec) = self.resolve_using_paths(&orig) {
204
+ import_decl.src.value = Wtf8Atom::from(new_spec);
205
+ import_decl.src.raw = None;
206
+ }
207
+ }
208
+ ModuleDecl::ExportAll(export_all) => {
209
+ let orig = export_all.src.value.to_string_lossy().to_string();
210
+ if let Some(new_spec) = self.resolve_using_paths(&orig) {
211
+ export_all.src.value = Wtf8Atom::from(new_spec);
212
+ export_all.src.raw = None;
213
+ }
214
+ }
215
+ ModuleDecl::ExportDecl(_) => {}
216
+ ModuleDecl::ExportNamed(named) => {
217
+ if let Some(src) = &mut named.src {
218
+ let orig = src.value.to_string_lossy().to_string();
219
+ if let Some(new_spec) = self.resolve_using_paths(&orig) {
220
+ src.value = Wtf8Atom::from(new_spec);
221
+ src.raw = None;
222
+ }
223
+ }
224
+ }
225
+ _ => {}
226
+ },
227
+ ModuleItem::Stmt(stmt) => {
228
+ // For statements, we still need to inspect call expressions (require)
229
+ stmt.visit_mut_children_with(self);
230
+ }
231
+ }
232
+ }
233
+
234
+ fn visit_mut_expr(&mut self, n: &mut Expr) {
235
+ // dynamic import: import("x")
236
+ if let Expr::Call(CallExpr { callee, args, .. }) = n {
237
+ // CommonJS require: require("x")
238
+ #[allow(clippy::collapsible_match)]
239
+ if let Callee::Expr(callee_expr) = callee {
240
+ if let Expr::Ident(ident) = &**callee_expr {
241
+ if &*ident.sym == "require" {
242
+ if let Some(ExprOrSpread { expr, .. }) = args.get_mut(0) {
243
+ if let Expr::Lit(Lit::Str(s)) = &mut **expr {
244
+ let orig = s.value.to_string_lossy().to_string();
245
+ if let Some(new_spec) = self.resolve_using_paths(&orig) {
246
+ s.value = Wtf8Atom::from(new_spec);
247
+ s.raw = None;
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ // For other expressions, recurse
257
+ n.visit_mut_children_with(self);
258
+ }
259
+ }
260
+
261
+ /// Try to find a file on disk for a candidate path. We try:
262
+ /// - the exact path
263
+ /// - path + .ts, .tsx, .js, .jsx, .d.ts
264
+ fn try_find_file_on_disk(candidate: &Path) -> Option<PathBuf> {
265
+ if candidate.exists() && candidate.is_file() {
266
+ return Some(std::fs::canonicalize(candidate).unwrap_or_else(|_| candidate.to_path_buf()));
267
+ }
268
+
269
+ static EXTS: [&str; 5] = ["ts", "tsx", "js", "jsx", "d.ts"];
270
+ for ext in &EXTS {
271
+ let mut p = candidate.to_path_buf();
272
+ // if candidate already has an extension, skip adding another
273
+ if candidate.extension().is_some() {
274
+ // already has extension and didn't exist, skip
275
+ continue;
276
+ }
277
+ p.set_extension(ext);
278
+ if p.exists() && p.is_file() {
279
+ return Some(std::fs::canonicalize(&p).unwrap_or(p));
280
+ }
281
+ }
282
+
283
+ // try index files if candidate is a directory
284
+ if candidate.is_dir() {
285
+ for ext in &["ts", "tsx", "js", "jsx"] {
286
+ let mut idx = candidate.to_path_buf();
287
+ idx.push(format!("index.{}", ext));
288
+ if idx.exists() && idx.is_file() {
289
+ return Some(std::fs::canonicalize(&idx).unwrap_or(idx));
290
+ }
291
+ }
292
+ }
293
+
294
+ None
295
+ }
296
+
297
+ /// Produce a specifier string to use in imports:
298
+ /// - If target is inside file_dir parent, produce a relative path (e.g., "./sub/foo")
299
+ /// - Otherwise produce a path prefixed with "./" and the path relative to base (fallback).
300
+ fn make_relative_or_prefixed(file_dir: &Path, target: &Path) -> String {
301
+ // Canonicalize both paths where possible so diff_paths returns a clean relative path
302
+ let target_abs = std::fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf());
303
+ let file_dir_abs = std::fs::canonicalize(file_dir).unwrap_or_else(|_| file_dir.to_path_buf());
304
+
305
+ // Try to compute a relative path from file_dir to target
306
+ let rel =
307
+ pathdiff::diff_paths(&target_abs, &file_dir_abs).unwrap_or_else(|| target_abs.clone());
308
+
309
+ let mut s = rel.to_string_lossy().to_string().replace('\\', "/");
310
+
311
+ // Ensure relative paths start with ./
312
+ if !s.starts_with('.') && !s.starts_with('/') {
313
+ s = format!("./{}", s);
314
+ }
315
+
316
+ // Strip common source extensions (keep behaviour consistent with TS/SWC expectations)
317
+ if s.ends_with(".d.ts") {
318
+ s.truncate(s.len() - 5);
319
+ } else if s.ends_with(".tsx") || s.ends_with(".jsx") {
320
+ s.truncate(s.len() - 4);
321
+ } else if s.ends_with(".ts") || s.ends_with(".js") {
322
+ s.truncate(s.len() - 3);
323
+ }
324
+
325
+ s
326
+ }
327
+
328
+ /// TypeScript to JavaScript compiler backed by SWC.
329
+ ///
330
+ /// `TypeScriptCompiler` wraps SWC parsing, transforms and codegen to produce
331
+ /// JavaScript output and an optional source map. It is configured using
332
+ /// `TsConfigOptions` so the host can control whether source maps are
333
+ /// emitted and which ECMAScript target is selected.
334
+ pub struct TypeScriptCompiler {
335
+ ts_config: TsConfigOptions,
336
+ }
337
+
338
+ impl TypeScriptCompiler {
339
+ /// Create a new compiler configured by `ts_config`.
340
+ pub fn new(ts_config: TsConfigOptions) -> Self {
341
+ Self { ts_config }
342
+ }
343
+
344
+ /// Compile a single TypeScript file to JavaScript.
345
+ pub fn compile_file(&self, input: &Path, output: &Path) -> Result<(), String> {
346
+ let cm: Lrc<SourceMap> = Default::default();
347
+
348
+ // Capture errors in a buffer instead of printing to stderr
349
+ let error_buffer = ErrorBuffer::new();
350
+ let error_buffer_clone = error_buffer.clone();
351
+
352
+ let emitter = EmitterWriter::new(Box::new(error_buffer), Some(cm.clone()), false, true);
353
+ let handler = Handler::with_emitter(true, false, Box::new(emitter));
354
+
355
+ let fm = cm
356
+ .load_file(input)
357
+ .map_err(|e| format!("Failed to load input {}: {}", input.display(), e))?;
358
+
359
+ let comments = SingleThreadedComments::default();
360
+
361
+ // Parse TypeScript (no TSX support - backend only)
362
+ let lexer = Lexer::new(
363
+ Syntax::Typescript(TsSyntax {
364
+ tsx: false,
365
+ ..Default::default()
366
+ }),
367
+ self.ts_config.target,
368
+ StringInput::from(&*fm),
369
+ Some(&comments),
370
+ );
371
+
372
+ let mut parser = Parser::new_from(lexer);
373
+
374
+ for e in parser.take_errors() {
375
+ e.into_diagnostic(&handler).emit();
376
+ }
377
+
378
+ let module = parser.parse_program().map_err(|e| {
379
+ e.into_diagnostic(&handler).emit();
380
+
381
+ let error_msg = error_buffer_clone.get_content();
382
+ if error_msg.is_empty() {
383
+ format!("Failed to parse {}", input.display())
384
+ } else {
385
+ format!("\n{}", error_msg.trim())
386
+ }
387
+ })?;
388
+
389
+ // Apply transformations and generate code + optional sourcemap
390
+ let globals = Globals::default();
391
+ let (code, map_opt) = GLOBALS
392
+ .set(&globals, || {
393
+ self.transform_and_generate(module, &cm, &comments, input)
394
+ })
395
+ .map_err(|e| format!("Transformation error: {:?}", e))?;
396
+
397
+ // Write output with optional sourcemap
398
+ if self.ts_config.emit_sourcemap {
399
+ if let Some(map) = map_opt {
400
+ write_sourcemap_and_code(output, &code, Some(map))?;
401
+ } else {
402
+ std::fs::write(output, code)
403
+ .map_err(|e| format!("Failed to write output {}: {}", output.display(), e))?;
404
+ }
405
+ } else {
406
+ std::fs::write(output, code)
407
+ .map_err(|e| format!("Failed to write output {}: {}", output.display(), e))?;
408
+ }
409
+
410
+ Ok(())
411
+ }
412
+
413
+ /// Apply SWC transforms (resolver, strip, CommonJS conversion, hygiene)
414
+ /// and perform code generation.
415
+ ///
416
+ /// Now accepts `input` to compute relative paths when rewriting imports.
417
+ fn transform_and_generate(
418
+ &self,
419
+ module: swc_ecma_ast::Program,
420
+ cm: &Lrc<SourceMap>,
421
+ comments: &SingleThreadedComments,
422
+ input: &Path,
423
+ ) -> Result<(String, Option<String>), String> {
424
+ let unresolved_mark = Mark::new();
425
+ let top_level_mark = Mark::new();
426
+
427
+ // Start from Program (owned)
428
+ let mut program = module;
429
+
430
+ // If ts_config defines base_url or paths, run our paths rewriter
431
+ if let Some(base_url) = &self.ts_config.base_url {
432
+ let compiled_paths = &self.ts_config.paths; // Used direct field access
433
+ if !compiled_paths.is_empty() {
434
+ let rewriter = PathsRewriter {
435
+ base_url: base_url.clone(),
436
+ paths: compiled_paths.clone(),
437
+ file_dir: input
438
+ .parent()
439
+ .map(PathBuf::from)
440
+ .unwrap_or_else(|| PathBuf::from(".")),
441
+ };
442
+
443
+ // visit mutably to rewrite specifiers
444
+ let mut rewriter = rewriter;
445
+ program.visit_mut_with(&mut rewriter);
446
+ }
447
+ }
448
+
449
+ // agora aplique as transforms normais
450
+ let module = program.apply(resolver(unresolved_mark, top_level_mark, true));
451
+ let module = module.apply(strip(unresolved_mark, top_level_mark));
452
+ let module = module.apply(common_js(
453
+ PathResolverEnum::Default,
454
+ unresolved_mark,
455
+ swc_ecma_transforms_module::util::Config::default(),
456
+ swc_ecma_transforms_module::common_js::FeatureFlag::default(),
457
+ ));
458
+ let module = module.apply(hygiene());
459
+ let program = module.apply(fixer(Some(comments)));
460
+
461
+ // NOTE: buffer de mappings como Vec<(BytePos, LineCol)>
462
+ let mut src_map_buf: Vec<(BytePos, LineCol)> = Vec::new();
463
+ let mut code_buf: Vec<u8> = Vec::new();
464
+
465
+ {
466
+ let js_writer = JsWriter::new(cm.clone(), "\n", &mut code_buf, Some(&mut src_map_buf));
467
+ let mut emitter = CodegenEmitter {
468
+ cfg: Default::default(),
469
+ cm: cm.clone(),
470
+ comments: Some(comments),
471
+ wr: Box::new(js_writer),
472
+ };
473
+
474
+ emitter
475
+ .emit_program(&program)
476
+ .map_err(|e| format!("codegen emit error: {:?}", e))?;
477
+ }
478
+
479
+ let code = String::from_utf8(code_buf).map_err(|e| format!("code not utf8: {:?}", e))?;
480
+
481
+ let map = if !src_map_buf.is_empty() {
482
+ let sm = cm.build_source_map(&src_map_buf, None, SourceMapConfigImpl);
483
+ let mut s = Vec::new();
484
+ sm.to_writer(&mut s)
485
+ .map_err(|e| format!("failed to write source map file: {:?}", e))?;
486
+ Some(String::from_utf8(s).map_err(|e| format!("source map not utf8: {:?}", e))?)
487
+ } else {
488
+ None
489
+ };
490
+
491
+ Ok((code, map))
492
+ }
493
+ }
@@ -0,0 +1,67 @@
1
+ use std::path::PathBuf;
2
+
3
+ use super::tsconfig::{parse_tsconfig, TsConfigOptions};
4
+
5
+ /// Build configuration for the TypeScript compiler.
6
+ ///
7
+ /// `BuildConfig` holds paths and parsed TypeScript options used by the
8
+ /// native builder. It also provides convenient helpers to compute output
9
+ /// locations and string representations used in other modules.
10
+ #[derive(Clone, Debug)]
11
+ pub struct BuildConfig {
12
+ /// Root directory containing the source files to compile.
13
+ pub source_root: PathBuf,
14
+
15
+ /// Output root directory where compiled assets will be written.
16
+ pub out_root: PathBuf,
17
+
18
+ /// Parsed tsconfig options used to configure the compiler.
19
+ pub ts_config: TsConfigOptions,
20
+
21
+ /// Glob patterns that should be ignored when scanning source files.
22
+ pub ignore_patterns: Vec<String>,
23
+ }
24
+
25
+ impl BuildConfig {
26
+ /// Create a new `BuildConfig` from `source_root` and `out_root` strings.
27
+ ///
28
+ /// This function attempts to parse `tsconfig.json` in the current
29
+ /// working directory to populate compiler options. It returns an error
30
+ /// string when parsing fails.
31
+ pub fn new(source_root: String, out_root: String) -> Result<Self, String> {
32
+ let tsconfig_path = PathBuf::from("tsconfig.json");
33
+ let ts_config = parse_tsconfig(Some(&tsconfig_path))?;
34
+
35
+ let source_root = PathBuf::from(source_root);
36
+ let out_root = PathBuf::from(out_root);
37
+
38
+ Ok(Self {
39
+ source_root,
40
+ out_root,
41
+ ts_config,
42
+ ignore_patterns: vec![".test.ts".to_string(), ".spec.ts".to_string()],
43
+ })
44
+ }
45
+
46
+ /// Return the `source_root` as a UTF-8 lossily converted `String`.
47
+ pub fn source_root_str(&self) -> String {
48
+ self.source_root.to_string_lossy().to_string()
49
+ }
50
+
51
+ /// Return the `out_root` as a UTF-8 lossily converted `String`.
52
+ pub fn output_path_str(&self) -> String {
53
+ self.out_root.to_string_lossy().to_string()
54
+ }
55
+
56
+ /// Compute the output path for a given input relative path.
57
+ ///
58
+ /// The function appends `input_relative` to the `out_root` and changes
59
+ /// the extension to `.js` (e.g. `src/users/route.ts` ->
60
+ /// `<out_root>/src/users/route.js`).
61
+ pub fn compute_output_path(&self, input_relative: &str) -> PathBuf {
62
+ let mut out_path = self.out_root.clone();
63
+ out_path.push(input_relative);
64
+ out_path.set_extension("js");
65
+ out_path
66
+ }
67
+ }
@@ -0,0 +1,126 @@
1
+ //! Native builder entrypoints and orchestration.
2
+ //!
3
+ //! This module exposes the `build_project` function which is invoked from the
4
+ //! host (Node) via N-API. It wires together scanning, compilation and route
5
+ //! manifest generation using the Rust-based SWC compiler integration.
6
+ //!
7
+ //! Exposes the native `build_project` entrypoint used by the host.
8
+
9
+ use napi_derive::napi;
10
+ use rayon::prelude::*;
11
+ use std::{fs, time::Instant};
12
+
13
+ pub mod compiler;
14
+ pub mod config;
15
+ pub mod sourcemap;
16
+ pub mod tsconfig;
17
+ pub mod types;
18
+
19
+ #[cfg(test)]
20
+ mod tests;
21
+
22
+ use compiler::TypeScriptCompiler;
23
+ use config::BuildConfig;
24
+ use types::{BuildResult, CompileResult};
25
+
26
+ #[napi]
27
+ /// Build the project located at `source_root` and emit outputs to `out_root`.
28
+ ///
29
+ /// This function is exported to the host via N-API and performs the full
30
+ /// native compilation pipeline:
31
+ /// 1. Reads build configuration from `source_root`.
32
+ /// 2. Scans for TypeScript files matching `.ts`.
33
+ /// 3. Compiles files (in parallel) using the embedded SWC-based compiler.
34
+ /// 4. Aggregates compilation results and fails the build if there are errors.
35
+ /// 5. If route files exist in the output, produces a `routes.json` manifest
36
+ /// containing route metadata consumed by the runtime.
37
+ ///
38
+ /// Errors are returned as `napi::Error` to be propagated to the host.
39
+ /// High-level build entrypoint for the native TypeScript builder.
40
+ ///
41
+ /// `build_project` coordinates scanning the source tree, applying route
42
+ /// conventions, and producing a `RoutesManifest` that can be consumed by the
43
+ /// runtime. Currently this function is a thin wrapper and may be expanded to
44
+ /// run parallel compilation and emit artifacts to disk.
45
+ pub fn build_project(source_root: String, out_root: String) -> napi::Result<()> {
46
+ let start = Instant::now();
47
+
48
+ // Load configuration
49
+ let config =
50
+ BuildConfig::new(source_root, out_root).map_err(napi::Error::from_reason)?;
51
+
52
+ fs::remove_dir_all(&config.out_root).ok();
53
+
54
+ // Scan TypeScript files using glob patterns
55
+ use crate::scanner::FileScanner;
56
+ let ts_files = crate::scanner::NativeFileScanner::new()
57
+ .scan_dir(
58
+ &[config.source_root_str()],
59
+ Some(crate::scanner::ScanOptions {
60
+ include: Some(vec!["**/*.ts".to_string()]),
61
+ ignore: Some(config.ignore_patterns.clone()),
62
+ }),
63
+ )
64
+ .map_err(|e| napi::Error::from_reason(format!("scan failed: {}", e)))?;
65
+
66
+ // Compile files in parallel
67
+ let compiler = TypeScriptCompiler::new(config.ts_config.clone());
68
+
69
+ let results: Vec<Result<CompileResult, String>> = ts_files
70
+ .par_iter()
71
+ .map(|file| {
72
+ let output_path = config.compute_output_path(&file.path);
73
+
74
+ if let Some(parent) = output_path.parent() {
75
+ std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
76
+ }
77
+
78
+ let file_start = Instant::now();
79
+ compiler
80
+ .compile_file(std::path::Path::new(&file.full_path), &output_path)
81
+ .map(|_| CompileResult {
82
+ output_path: output_path.to_string_lossy().to_string(),
83
+ duration_ms: file_start.elapsed().as_secs_f64() * 1000.0,
84
+ })
85
+ })
86
+ .collect();
87
+
88
+ // Aggregate results
89
+ let mut build_result = BuildResult::new(start.elapsed().as_secs_f64() * 1000.0);
90
+ build_result.files_compiled = ts_files.len();
91
+
92
+ for result in results {
93
+ match result {
94
+ Ok(timing) => build_result.timings.push(timing),
95
+ Err(e) => build_result.failures.push(e),
96
+ }
97
+ }
98
+
99
+ // Build summary is emitted to the host (Node) via the native API; avoid printing here.
100
+
101
+ if build_result.has_failures() {
102
+ let failures_msg = build_result
103
+ .failures
104
+ .iter()
105
+ .take(5)
106
+ .map(|e| e.as_str())
107
+ .collect::<Vec<_>>()
108
+ .join("\n\n");
109
+
110
+ return Err(napi::Error::from_reason(format!(
111
+ "Build completed with {} failures:\n\n{}",
112
+ build_result.failures.len(),
113
+ failures_msg
114
+ )));
115
+ }
116
+
117
+ build_result.total_duration_ms = start.elapsed().as_secs_f64() * 1000.0;
118
+
119
+ // Generate routes manifest using helper
120
+ crate::router::write_routes_manifest(&config).map_err(napi::Error::from_reason)?;
121
+
122
+ // Events manifest: build from already-scanned `ts_files` (no extra scan)
123
+ crate::events::write_events_manifest(&config).map_err(napi::Error::from_reason)?;
124
+
125
+ Ok(())
126
+ }