@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,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
|
+
}
|