@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,36 @@
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
+ }
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,100 @@
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
+ }
@@ -0,0 +1,53 @@
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
+ }
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,87 @@
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;
@@ -0,0 +1,113 @@
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
+ }
@@ -0,0 +1,41 @@
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
+ }
@@ -0,0 +1,78 @@
1
+ use regex::Regex;
2
+
3
+ pub trait PathTransformer {
4
+ /// Normalize a file path by removing extensions, groups, and normalizing
5
+ /// separators.
6
+ fn normalize(&self, path: &str) -> String;
7
+
8
+ /// Normalize a path and apply an optional global prefix, returning a
9
+ /// value that always starts with a leading slash.
10
+ fn normalize_path(&self, path: &str, global_prefix: &str) -> String;
11
+
12
+ /// Clone the transformer as a boxed trait object.
13
+ fn clone_box(&self) -> Box<dyn PathTransformer>;
14
+ }
15
+
16
+ #[derive(Clone)]
17
+ pub struct NativeEventTransformer {
18
+ remove_ext: Regex,
19
+ remove_groups: Regex,
20
+ }
21
+
22
+ impl NativeEventTransformer {
23
+ pub fn new() -> Self {
24
+ Self {
25
+ remove_ext: Regex::new(r"\.[A-Za-z0-9]+$").unwrap(),
26
+ remove_groups: Regex::new(r"\(([^(/\\]+)\)[/\\]").unwrap(),
27
+ }
28
+ }
29
+ }
30
+
31
+ impl PathTransformer for NativeEventTransformer {
32
+ /// Normalize an event file path: remove extension, remove groups and
33
+ /// normalize separators to `/`.
34
+ fn normalize(&self, path: &str) -> String {
35
+ let mut s = path.to_string();
36
+ s = self.remove_ext.replace(&s, "").to_string();
37
+ s = self.remove_groups.replace_all(&s, "").to_string();
38
+ s.replace('\\', "/")
39
+ }
40
+
41
+ /// Normalize a path and apply an optional global prefix, returning a
42
+ /// value that always starts with a leading slash.
43
+ fn normalize_path(&self, path: &str, global_prefix: &str) -> String {
44
+ let combined = if global_prefix.is_empty() {
45
+ path.to_string()
46
+ } else {
47
+ format!("{}/{}", global_prefix.trim_end_matches('/'), path.trim_start_matches('/'))
48
+ };
49
+
50
+ let no_trailing = if combined.ends_with('/') && combined.len() > 1 {
51
+ combined.trim_end_matches('/').to_string()
52
+ } else {
53
+ combined
54
+ };
55
+
56
+ if no_trailing.starts_with('/') {
57
+ no_trailing
58
+ } else {
59
+ format!("/{}", no_trailing)
60
+ }
61
+ }
62
+
63
+ fn clone_box(&self) -> Box<dyn PathTransformer> {
64
+ Box::new(self.clone())
65
+ }
66
+ }
67
+
68
+ #[cfg(test)]
69
+ mod tests {
70
+ use super::*;
71
+
72
+ #[test]
73
+ fn normalize_basic() {
74
+ let t = NativeEventTransformer::new();
75
+ assert_eq!(t.normalize("app/events/chat/message.ts"), "app/events/chat/message");
76
+ assert_eq!(t.normalize("(v1)/events/connection.ts"), "events/connection");
77
+ }
78
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,54 @@
1
+ //! N-API bindings exported by the native crate.
2
+ //!
3
+ //! This module exposes a small surface area to the host (Node) so JavaScript
4
+ //! code can invoke the native builder and scanner implemented in Rust.
5
+ //! Prefer using the higher-level functions exported here rather than calling
6
+ //! into the modules directly from the host.
7
+
8
+ use napi_derive::napi;
9
+
10
+ mod builder;
11
+ mod router;
12
+ mod scanner;
13
+ mod events;
14
+
15
+ /// Compile the project and emit artifacts.
16
+ ///
17
+ /// This re-export exposes the native `build_project` entrypoint implemented
18
+ /// in the `builder` module. The function is intended to be invoked from the
19
+ /// host via N-API and performs scanning, compilation and manifest emission.
20
+ pub use builder::build_project;
21
+
22
+ /// Route and manifest types produced by the native router processor.
23
+ pub use router::{Route, RoutesManifest};
24
+
25
+ /// Scanner types returned by `scan_dir`.
26
+ pub use scanner::{FileInfo, ScanOptions};
27
+
28
+ use crate::scanner::FileScanner;
29
+
30
+ /// Scan a directory tree for files matching the provided `path_components`.
31
+ ///
32
+ /// `path_components` is a vector of path segments used as roots for the
33
+ /// scanner (for example `["examples/1-basic-api"]`). `options` may include
34
+ /// include/ignore globs. Returns a list of `FileInfo` describing discovered
35
+ /// files or an error which is converted into a `napi::Error` for the host.
36
+ #[napi]
37
+ pub fn scan_dir(
38
+ path_components: Vec<String>,
39
+ options: Option<ScanOptions>,
40
+ ) -> napi::Result<Vec<FileInfo>> {
41
+ let scanner = scanner::NativeFileScanner::new();
42
+ scanner
43
+ .scan_dir(&path_components, options)
44
+ .map_err(|e| napi::Error::from_reason(format!("scan failed: {}", e)))
45
+ }
46
+
47
+ /// Return the native crate version embedded at compile time.
48
+ ///
49
+ /// This returns the value of `CARGO_PKG_VERSION` so the host can verify the
50
+ /// native binary version matches expectations.
51
+ #[napi]
52
+ pub fn schema_version() -> &'static str {
53
+ env!("CARGO_PKG_VERSION")
54
+ }