@lithia-js/native 1.0.0-canary.2 → 1.0.0-canary.4
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 +13 -0
- package/package.json +16 -16
- package/Cargo.toml +0 -31
- 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
package/src/builder/mod.rs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
}
|
package/src/builder/sourcemap.rs
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
use std::path::Path;
|
|
2
|
-
|
|
3
|
-
/// Write compiled `code` and its optional `source_map` to disk.
|
|
4
|
-
///
|
|
5
|
-
/// When `source_map` is `Some`, this function writes both the `.js` file and
|
|
6
|
-
/// the corresponding `.js.map` alongside it. The `.js` file will include a
|
|
7
|
-
/// `//# sourceMappingURL=<file>.map` comment so runtimes and devtools can
|
|
8
|
-
/// automatically discover the map. If `source_map` is `None`, only the
|
|
9
|
-
/// `.js` file is written.
|
|
10
|
-
pub fn write_sourcemap_and_code(output: &Path, code: &str, source_map: Option<String>) -> Result<(), String> {
|
|
11
|
-
let map_file_name = format!(
|
|
12
|
-
"{}.map",
|
|
13
|
-
output
|
|
14
|
-
.file_name()
|
|
15
|
-
.ok_or("Invalid output file name")?
|
|
16
|
-
.to_string_lossy()
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
// When a sourcemap is present, append the sourceMappingURL comment.
|
|
20
|
-
let code_with_map = if source_map.is_some() {
|
|
21
|
-
format!("{}\n//# sourceMappingURL={}\n", code, map_file_name)
|
|
22
|
-
} else {
|
|
23
|
-
code.to_string()
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
std::fs::write(output, code_with_map)
|
|
27
|
-
.map_err(|e| format!("Failed to write output file: {}", e))?;
|
|
28
|
-
|
|
29
|
-
if let Some(map) = source_map {
|
|
30
|
-
let map_path = output.with_extension("js.map");
|
|
31
|
-
std::fs::write(&map_path, map)
|
|
32
|
-
.map_err(|e| format!("Failed to write sourcemap file: {}", e))?;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
Ok(())
|
|
36
|
-
}
|
package/src/builder/tests.rs
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
use super::compiler::TypeScriptCompiler;
|
|
2
|
-
use super::tsconfig::TsConfigOptions;
|
|
3
|
-
use std::fs;
|
|
4
|
-
use swc_ecma_ast::EsVersion;
|
|
5
|
-
use tempfile::TempDir;
|
|
6
|
-
|
|
7
|
-
#[test]
|
|
8
|
-
fn test_compile_with_paths() {
|
|
9
|
-
let dir = TempDir::new().unwrap();
|
|
10
|
-
let root = dir.path();
|
|
11
|
-
|
|
12
|
-
let src = root.join("src");
|
|
13
|
-
fs::create_dir(&src).unwrap();
|
|
14
|
-
|
|
15
|
-
let services = src.join("services");
|
|
16
|
-
fs::create_dir(&services).unwrap();
|
|
17
|
-
|
|
18
|
-
// Define target file that the alias points to
|
|
19
|
-
fs::write(services.join("user-service.ts"), "export class UserService {}").unwrap();
|
|
20
|
-
|
|
21
|
-
// Define source file interacting with alias
|
|
22
|
-
let input_path = src.join("main.ts");
|
|
23
|
-
fs::write(&input_path, "import { UserService } from '@/services/user-service'; console.log(UserService);").unwrap();
|
|
24
|
-
|
|
25
|
-
let dist = root.join("dist");
|
|
26
|
-
fs::create_dir(&dist).unwrap();
|
|
27
|
-
let output_path = dist.join("main.js");
|
|
28
|
-
|
|
29
|
-
// Setup compiler with path mapping
|
|
30
|
-
// Mapping: "@/*" -> ["./src/*"]
|
|
31
|
-
// BaseUrl: root
|
|
32
|
-
let ts_config = TsConfigOptions {
|
|
33
|
-
emit_sourcemap: false,
|
|
34
|
-
target: EsVersion::Es2020,
|
|
35
|
-
base_url: Some(root.to_path_buf()),
|
|
36
|
-
paths: vec![("@/*".to_string(), vec!["./src/*".to_string()])],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
let compiler = TypeScriptCompiler::new(ts_config);
|
|
40
|
-
|
|
41
|
-
// Compile
|
|
42
|
-
compiler.compile_file(&input_path, &output_path).expect("compile failed");
|
|
43
|
-
|
|
44
|
-
// Check output based on behavior
|
|
45
|
-
let js = fs::read_to_string(&output_path).unwrap();
|
|
46
|
-
|
|
47
|
-
assert!(js.contains(r#"require("./services/user-service")"#), "Output JS did not contain expected relative require. Got:\n{}", js);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
#[test]
|
|
51
|
-
fn test_compile_no_paths() {
|
|
52
|
-
let dir = TempDir::new().unwrap();
|
|
53
|
-
let root = dir.path();
|
|
54
|
-
let src = root.join("src");
|
|
55
|
-
fs::create_dir(&src).unwrap();
|
|
56
|
-
|
|
57
|
-
let input_path = src.join("index.ts");
|
|
58
|
-
// Normal relative import shouldn't change
|
|
59
|
-
fs::write(&input_path, "import { x } from './utils'; console.log(x);").unwrap();
|
|
60
|
-
fs::write(src.join("utils.ts"), "export const x = 1;").unwrap();
|
|
61
|
-
|
|
62
|
-
let dist = root.join("dist");
|
|
63
|
-
fs::create_dir(&dist).unwrap();
|
|
64
|
-
let output_path = dist.join("index.js");
|
|
65
|
-
|
|
66
|
-
let ts_config = TsConfigOptions {
|
|
67
|
-
emit_sourcemap: false,
|
|
68
|
-
target: EsVersion::Es2020,
|
|
69
|
-
base_url: None,
|
|
70
|
-
paths: vec![],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
let compiler = TypeScriptCompiler::new(ts_config);
|
|
74
|
-
compiler.compile_file(&input_path, &output_path).expect("compile failed");
|
|
75
|
-
|
|
76
|
-
let js = fs::read_to_string(&output_path).unwrap();
|
|
77
|
-
assert!(js.contains(r#"require("./utils")"#));
|
|
78
|
-
}
|
package/src/builder/tsconfig.rs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
use serde_json::Value;
|
|
2
|
-
use std::path::Path;
|
|
3
|
-
|
|
4
|
-
use swc_ecma_ast::EsVersion;
|
|
5
|
-
|
|
6
|
-
/// Parsed TypeScript configuration options relevant for the native builder.
|
|
7
|
-
/// This struct captures only the small subset of `tsconfig.json` that the
|
|
8
|
-
/// builder needs: whether to emit source maps and the target ECMAScript
|
|
9
|
-
/// version.
|
|
10
|
-
#[derive(Debug, Clone)]
|
|
11
|
-
pub struct TsConfigOptions {
|
|
12
|
-
/// Whether to generate source maps during compilation.
|
|
13
|
-
pub emit_sourcemap: bool,
|
|
14
|
-
|
|
15
|
-
/// SWC `EsVersion` target inferred from `compilerOptions.target`.
|
|
16
|
-
pub target: EsVersion,
|
|
17
|
-
|
|
18
|
-
/// Base URL for non-relative module resolution.
|
|
19
|
-
pub base_url: Option<std::path::PathBuf>,
|
|
20
|
-
|
|
21
|
-
/// Path mappings for module resolution.
|
|
22
|
-
pub paths: Vec<(String, Vec<String>)>,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Parse a `tsconfig.json` file from `path` (optional) and return
|
|
26
|
-
/// `TsConfigOptions` used by the builder.
|
|
27
|
-
///
|
|
28
|
-
/// If `path` is `None`, the function will attempt to read `tsconfig.json`
|
|
29
|
-
/// from the current working directory. If the file does not exist or cannot be
|
|
30
|
-
/// parsed, reasonable defaults are returned and an `Ok` result is produced
|
|
31
|
-
/// (the builder treats missing or invalid config as non-fatal by design).
|
|
32
|
-
pub fn parse_tsconfig(path: Option<&Path>) -> Result<TsConfigOptions, String> {
|
|
33
|
-
let default_config = TsConfigOptions {
|
|
34
|
-
emit_sourcemap: true,
|
|
35
|
-
target: EsVersion::Es2022,
|
|
36
|
-
base_url: None,
|
|
37
|
-
paths: Vec::new(),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
let path = match path {
|
|
41
|
-
Some(p) => p.to_path_buf(),
|
|
42
|
-
None => {
|
|
43
|
-
let cwd = std::env::current_dir().unwrap();
|
|
44
|
-
cwd.join("tsconfig.json")
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
if !path.exists() {
|
|
49
|
-
// do not print from native side; using default config
|
|
50
|
-
return Ok(default_config);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let s = std::fs::read_to_string(&path)
|
|
54
|
-
.map_err(|e| format!("failed to read tsconfig {}: {}", path.display(), e))?;
|
|
55
|
-
|
|
56
|
-
let v: Value = serde_json::from_str(&s)
|
|
57
|
-
.map_err(|e| format!("failed to parse tsconfig {}: {}", path.display(), e))?;
|
|
58
|
-
|
|
59
|
-
// Hardcoded options as per requirements
|
|
60
|
-
let emit_sourcemap = true;
|
|
61
|
-
let target = EsVersion::Es2022;
|
|
62
|
-
|
|
63
|
-
let mut base_url = None;
|
|
64
|
-
let mut paths = Vec::new();
|
|
65
|
-
|
|
66
|
-
if let Some(opts) = v.get("compilerOptions") {
|
|
67
|
-
// Enforce sourceMap and target:
|
|
68
|
-
// We do *not* read sourceMap or target from tsconfig anymore.
|
|
69
|
-
// They are always true and ES2022 respectively.
|
|
70
|
-
|
|
71
|
-
if let Some(base) = opts.get("baseUrl").and_then(|v| v.as_str()) {
|
|
72
|
-
let tsconfig_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
|
73
|
-
base_url = Some(tsconfig_dir.join(base));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if let Some(p) = opts.get("paths").and_then(|v| v.as_object()) {
|
|
77
|
-
// implicit baseUrl if paths are present but baseUrl is missing
|
|
78
|
-
if base_url.is_none() {
|
|
79
|
-
let tsconfig_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
|
80
|
-
base_url = Some(tsconfig_dir.to_path_buf());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (k, v) in p {
|
|
84
|
-
let targets = if let Some(arr) = v.as_array() {
|
|
85
|
-
arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()
|
|
86
|
-
} else {
|
|
87
|
-
vec![]
|
|
88
|
-
};
|
|
89
|
-
paths.push((k.clone(), targets));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
Ok(TsConfigOptions {
|
|
95
|
-
emit_sourcemap,
|
|
96
|
-
target,
|
|
97
|
-
base_url,
|
|
98
|
-
paths,
|
|
99
|
-
})
|
|
100
|
-
}
|
package/src/builder/types.rs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/// Result information for a single compiled file.
|
|
2
|
-
/// `CompileResult` contains small statistics about an individual compilation
|
|
3
|
-
/// unit such as the `output_path` produced and the time taken in
|
|
4
|
-
/// milliseconds. Fields are public to allow the caller to serialize or log
|
|
5
|
-
/// results as needed.
|
|
6
|
-
#[derive(Debug, Clone)]
|
|
7
|
-
pub struct CompileResult {
|
|
8
|
-
#[allow(dead_code)]
|
|
9
|
-
/// Absolute or relative filesystem path to the compiled output.
|
|
10
|
-
pub output_path: String,
|
|
11
|
-
#[allow(dead_code)]
|
|
12
|
-
/// Duration of the compilation in milliseconds.
|
|
13
|
-
pub duration_ms: f64,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/// Overall build result with statistics.
|
|
17
|
-
/// `BuildResult` aggregates per-file `CompileResult` entries and provides a
|
|
18
|
-
/// simple API to inspect whether failures occurred, and totals such as the
|
|
19
|
-
/// number of files compiled and total duration.
|
|
20
|
-
#[derive(Debug)]
|
|
21
|
-
pub struct BuildResult {
|
|
22
|
-
/// Number of files that were processed during the build.
|
|
23
|
-
pub files_compiled: usize,
|
|
24
|
-
|
|
25
|
-
/// Collected failure messages for files that failed to compile.
|
|
26
|
-
pub failures: Vec<String>,
|
|
27
|
-
|
|
28
|
-
/// Timeline entries for successful compilations.
|
|
29
|
-
pub timings: Vec<CompileResult>,
|
|
30
|
-
|
|
31
|
-
/// Total elapsed build time in milliseconds.
|
|
32
|
-
pub total_duration_ms: f64,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
impl BuildResult {
|
|
36
|
-
pub fn new(total_duration_ms: f64) -> Self {
|
|
37
|
-
Self {
|
|
38
|
-
files_compiled: 0,
|
|
39
|
-
failures: Vec::new(),
|
|
40
|
-
timings: Vec::new(),
|
|
41
|
-
total_duration_ms,
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
pub fn has_failures(&self) -> bool {
|
|
46
|
-
!self.failures.is_empty()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#[allow(dead_code)]
|
|
50
|
-
pub fn success_count(&self) -> usize {
|
|
51
|
-
self.files_compiled - self.failures.len()
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/events/convention.rs
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
use crate::events::transformer::{NativeEventTransformer, PathTransformer};
|
|
2
|
-
|
|
3
|
-
/// Trait that defines how filesystem event filenames are converted into a
|
|
4
|
-
/// normalized event identifier used by the runtime.
|
|
5
|
-
pub trait EventConvention {
|
|
6
|
-
/// Transform a filesystem path (relative, may include `app/events/...`)
|
|
7
|
-
/// into a normalized event path (no extension, forward slashes).
|
|
8
|
-
fn transform_path(&self, path: &str) -> String;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/// Native event convention implementation that reuses the router's
|
|
12
|
-
/// `PathTransformer` for common filesystem transformations and then
|
|
13
|
-
/// returns a normalized path used to derive the event name.
|
|
14
|
-
pub struct NativeEventConvention {
|
|
15
|
-
transformer: Box<dyn PathTransformer>,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
impl NativeEventConvention {
|
|
19
|
-
pub fn new(transformer: Option<Box<dyn PathTransformer>>) -> Self {
|
|
20
|
-
let transformer = transformer.unwrap_or_else(|| Box::new(NativeEventTransformer::new()));
|
|
21
|
-
Self { transformer }
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
impl EventConvention for NativeEventConvention {
|
|
26
|
-
fn transform_path(&self, path: &str) -> String {
|
|
27
|
-
// Remove leading prefix if present
|
|
28
|
-
let mut p = path.trim_start_matches("app/events/").to_string();
|
|
29
|
-
|
|
30
|
-
// Delegate file -> normalized path transformations to the event transformer
|
|
31
|
-
p = self.transformer.normalize(&p);
|
|
32
|
-
|
|
33
|
-
// Ensure forward slashes
|
|
34
|
-
p.replace('\\', "/")
|
|
35
|
-
}
|
|
36
|
-
}
|
package/src/events/mod.rs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
use napi_derive::napi;
|
|
2
|
-
use serde::Serialize;
|
|
3
|
-
|
|
4
|
-
use crate::builder::config::BuildConfig;
|
|
5
|
-
use crate::scanner::FileScanner;
|
|
6
|
-
use crate::schema_version;
|
|
7
|
-
use std::fs;
|
|
8
|
-
|
|
9
|
-
pub mod convention;
|
|
10
|
-
pub mod processor;
|
|
11
|
-
pub mod transformer;
|
|
12
|
-
|
|
13
|
-
/// Serializable event representation sent to the host (N-API).
|
|
14
|
-
#[napi(object)]
|
|
15
|
-
#[derive(Serialize, Debug, Clone)]
|
|
16
|
-
#[serde(rename_all = "camelCase")]
|
|
17
|
-
pub struct Event {
|
|
18
|
-
/// Event name (e.g., "chat:message" or "connection")
|
|
19
|
-
pub name: String,
|
|
20
|
-
|
|
21
|
-
/// Absolute filesystem path to the compiled handler (JS) file.
|
|
22
|
-
pub file_path: String,
|
|
23
|
-
|
|
24
|
-
/// Optional namespace (e.g., "chat" for "chat:message").
|
|
25
|
-
pub namespace: Option<String>,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/// Manifest containing all discovered events, serializable to the host.
|
|
29
|
-
#[napi(object)]
|
|
30
|
-
#[derive(Serialize, Debug)]
|
|
31
|
-
pub struct EventsManifest {
|
|
32
|
-
/// Manifest version string.
|
|
33
|
-
pub version: String,
|
|
34
|
-
|
|
35
|
-
/// List of events.
|
|
36
|
-
pub events: Vec<Event>,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// Generate and write `events.json` manifest based on scanned TypeScript files.
|
|
40
|
-
///
|
|
41
|
-
/// Uses the already-scanned `ts_files` (source files) and the provided
|
|
42
|
-
/// `BuildConfig` to map source files to their compiled output and produce the
|
|
43
|
-
/// final manifest next to `routes.json`.
|
|
44
|
-
pub fn write_events_manifest(config: &BuildConfig) -> Result<(), String> {
|
|
45
|
-
let events_path = config.out_root.join("app").join("events");
|
|
46
|
-
if !events_path.exists() {
|
|
47
|
-
return Ok(());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let event_files = crate::scanner::NativeFileScanner::new()
|
|
51
|
-
.scan_dir(
|
|
52
|
-
&[
|
|
53
|
-
config.output_path_str(),
|
|
54
|
-
"app".to_string(),
|
|
55
|
-
"events".to_string(),
|
|
56
|
-
],
|
|
57
|
-
Some(crate::scanner::ScanOptions {
|
|
58
|
-
include: Some(vec!["**/*.js".to_string()]),
|
|
59
|
-
ignore: None,
|
|
60
|
-
}),
|
|
61
|
-
)
|
|
62
|
-
.map_err(|e| format!("scan failed: {}", e))?;
|
|
63
|
-
|
|
64
|
-
let processor = processor::NativeEventProcessor::new(None, None);
|
|
65
|
-
let events = processor.process(&event_files);
|
|
66
|
-
|
|
67
|
-
if events.is_empty() {
|
|
68
|
-
return Ok(());
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let manifest = EventsManifest {
|
|
72
|
-
version: schema_version().to_string(),
|
|
73
|
-
events,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
let json = serde_json::to_string(&manifest)
|
|
77
|
-
.map_err(|e| format!("Failed to serialize events: {}", e))?;
|
|
78
|
-
|
|
79
|
-
let out = config.out_root.join("events.json");
|
|
80
|
-
fs::write(&out, json)
|
|
81
|
-
.map_err(|e| format!("Failed to write events manifest {}: {}", out.display(), e))?;
|
|
82
|
-
|
|
83
|
-
Ok(())
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
#[cfg(test)]
|
|
87
|
-
mod tests;
|
package/src/events/processor.rs
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
//! Event file processor utilities mirroring the router processor design.
|
|
2
|
-
//!
|
|
3
|
-
//! Converts scanned `FileInfo` entries for `app/events` into `Event`
|
|
4
|
-
//! structures suitable for serialization into `events.json`.
|
|
5
|
-
|
|
6
|
-
use crate::scanner::FileInfo;
|
|
7
|
-
|
|
8
|
-
use crate::events::convention::{EventConvention, NativeEventConvention};
|
|
9
|
-
use crate::events::transformer::{NativeEventTransformer, PathTransformer};
|
|
10
|
-
use crate::events::Event;
|
|
11
|
-
|
|
12
|
-
/// Trait that converts a discovered `FileInfo` into an `Event` structure.
|
|
13
|
-
pub trait EventProcessor {
|
|
14
|
-
fn process_event_file(&self, file: &FileInfo) -> Event;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/// Native implementation of `EventProcessor` that composes a
|
|
18
|
-
/// `PathTransformer` and an `EventConvention` similar to the route
|
|
19
|
-
/// processor.
|
|
20
|
-
pub struct NativeEventProcessor {
|
|
21
|
-
transformer: Box<dyn PathTransformer>,
|
|
22
|
-
convention: Box<dyn EventConvention>,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
impl NativeEventProcessor {
|
|
26
|
-
/// Create a new `NativeEventProcessor`.
|
|
27
|
-
/// Optional components may be provided for testing.
|
|
28
|
-
pub fn new(
|
|
29
|
-
opt_transformer: Option<Box<dyn PathTransformer>>,
|
|
30
|
-
opt_convention: Option<Box<dyn EventConvention>>,
|
|
31
|
-
) -> Self {
|
|
32
|
-
let transformer =
|
|
33
|
-
opt_transformer.unwrap_or_else(|| Box::new(NativeEventTransformer::new()));
|
|
34
|
-
let convention = opt_convention
|
|
35
|
-
.unwrap_or_else(|| Box::new(NativeEventConvention::new(Some(transformer.clone_box()))));
|
|
36
|
-
|
|
37
|
-
Self {
|
|
38
|
-
transformer,
|
|
39
|
-
convention,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// Convenience: process a collection of files into events.
|
|
44
|
-
pub fn process(&self, files: &[FileInfo]) -> Vec<Event> {
|
|
45
|
-
files.iter().map(|f| self.process_event_file(f)).collect()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
impl EventProcessor for NativeEventProcessor {
|
|
50
|
-
fn process_event_file(&self, file: &FileInfo) -> Event {
|
|
51
|
-
// Convert the scanned file path into a normalized event path using
|
|
52
|
-
// the convention and then apply the PathTransformer normalization
|
|
53
|
-
// (mirrors the route processor pipeline).
|
|
54
|
-
let intermediate = self.convention.transform_path(&file.path);
|
|
55
|
-
let normalized = self.transformer.normalize_path(&intermediate, "");
|
|
56
|
-
|
|
57
|
-
// Build event name: `a/b/c` -> `a:b:c` except for standalone names
|
|
58
|
-
let parts: Vec<&str> = normalized
|
|
59
|
-
.trim_start_matches('/')
|
|
60
|
-
.split('/')
|
|
61
|
-
.filter(|s| !s.is_empty())
|
|
62
|
-
.collect();
|
|
63
|
-
let event_name = if parts.is_empty() {
|
|
64
|
-
"".to_string()
|
|
65
|
-
} else if parts.len() == 1 {
|
|
66
|
-
parts[0].to_string()
|
|
67
|
-
} else {
|
|
68
|
-
let last = parts.last().unwrap();
|
|
69
|
-
if *last == "connection" || *last == "disconnect" {
|
|
70
|
-
last.to_string()
|
|
71
|
-
} else {
|
|
72
|
-
format!("{}:{}", parts[..parts.len() - 1].join(":"), last)
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
let namespace = if event_name.contains(":") {
|
|
77
|
-
Some(event_name.split(':').next().unwrap().to_string())
|
|
78
|
-
} else {
|
|
79
|
-
None
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
Event {
|
|
83
|
-
name: event_name,
|
|
84
|
-
file_path: file.full_path.clone(),
|
|
85
|
-
namespace,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#[cfg(test)]
|
|
91
|
-
mod tests {
|
|
92
|
-
use super::*;
|
|
93
|
-
use crate::scanner::FileInfo;
|
|
94
|
-
|
|
95
|
-
fn file_info(path: &str, full: &str) -> FileInfo {
|
|
96
|
-
FileInfo {
|
|
97
|
-
path: path.to_string(),
|
|
98
|
-
full_path: full.to_string(),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
#[test]
|
|
103
|
-
fn processor_creates_event() {
|
|
104
|
-
let p = NativeEventProcessor::new(None, None);
|
|
105
|
-
let f = file_info(
|
|
106
|
-
"app/events/chat/message.js",
|
|
107
|
-
"/out/app/events/chat/message.js",
|
|
108
|
-
);
|
|
109
|
-
let e = p.process_event_file(&f);
|
|
110
|
-
assert_eq!(e.name, "chat:message");
|
|
111
|
-
assert_eq!(e.namespace.unwrap(), "chat");
|
|
112
|
-
}
|
|
113
|
-
}
|
package/src/events/tests.rs
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#[cfg(test)]
|
|
2
|
-
mod tests {
|
|
3
|
-
use super::super::processor::NativeEventProcessor;
|
|
4
|
-
use crate::{events::processor::EventProcessor, scanner::FileInfo};
|
|
5
|
-
use std::fs;
|
|
6
|
-
use tempfile::TempDir;
|
|
7
|
-
|
|
8
|
-
#[test]
|
|
9
|
-
fn processor_generates_event_manifest_items() {
|
|
10
|
-
let temp = TempDir::new().unwrap();
|
|
11
|
-
let src_root = temp.path().join("src");
|
|
12
|
-
let out_root = temp.path().join("out");
|
|
13
|
-
fs::create_dir_all(&src_root).unwrap();
|
|
14
|
-
fs::create_dir_all(&out_root).unwrap();
|
|
15
|
-
|
|
16
|
-
// create a dummy tsconfig so BuildConfig::new can parse
|
|
17
|
-
fs::write(temp.path().join("tsconfig.json"), "{}").unwrap();
|
|
18
|
-
|
|
19
|
-
// Create a sample event file path entry
|
|
20
|
-
// Use compiled JS path as the processor expects files from the output
|
|
21
|
-
let relative = "app/events/chat/message.js".to_string();
|
|
22
|
-
let full = out_root.join(&relative).to_string_lossy().to_string();
|
|
23
|
-
|
|
24
|
-
// touch the compiled file on disk
|
|
25
|
-
fs::create_dir_all(std::path::Path::new(&full).parent().unwrap()).unwrap();
|
|
26
|
-
fs::write(&full, "module.exports = {};").unwrap();
|
|
27
|
-
|
|
28
|
-
let file_info = FileInfo {
|
|
29
|
-
path: relative.clone(),
|
|
30
|
-
full_path: full.clone(),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let processor = NativeEventProcessor::new(None, None);
|
|
34
|
-
let e = processor.process_event_file(&file_info);
|
|
35
|
-
|
|
36
|
-
assert_eq!(e.name, "chat:message");
|
|
37
|
-
assert_eq!(e.namespace.as_ref().unwrap(), "chat");
|
|
38
|
-
assert!(e.file_path.ends_with("message.js"));
|
|
39
|
-
assert_eq!(e.file_path, full);
|
|
40
|
-
}
|
|
41
|
-
}
|