Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/executeJS/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ native-tls = "0.2"
tokio-rustls = "0.26"
tokio-stream = "0.1"
flate2 = "1.1"
tempfile = "3.23.0"

# JavaScript 런타임 의존성 (Deno Core)
deno-runtime = { path = "../../../crates/deno-runtime" }
Expand Down
200 changes: 200 additions & 0 deletions apps/executeJS/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::js_executor::{execute_javascript_code, JsExecutionResult};
use serde::{Deserialize, Serialize};
use std::process::Command;

#[derive(Debug, Serialize, Deserialize)]
pub struct AppInfo {
Expand All @@ -9,6 +10,57 @@ pub struct AppInfo {
pub author: String,
}

// 린트 결과를 나타내는 구조체
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LintSeverity {
Error,
Warning,
Info,
Hint,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LintResult {
pub line: usize,
pub column: usize,
pub end_line: usize,
pub end_column: usize,
pub message: String,
pub severity: LintSeverity,
pub rule_id: String,
}

// oxlint JSON 출력 형식에 맞는 구조체
#[derive(Debug, Deserialize)]
struct OxlintOutput {
diagnostics: Vec<OxlintDiagnostic>,
}

// oxlint 진단 정보
#[derive(Debug, Deserialize)]
struct OxlintDiagnostic {
message: String,
code: String,
severity: String,
labels: Vec<OxlintLabel>,
}

// oxlint 레이블 (위치 정보 포함)
#[derive(Debug, Deserialize)]
struct OxlintLabel {
span: OxlintSpan,
}

// oxlint 스팬 (라인, 컬럼, 길이 정보)
#[derive(Debug, Deserialize)]
struct OxlintSpan {
line: usize,
column: usize,
#[serde(default)]
length: usize,
}

#[tauri::command]
pub async fn execute_js(code: &str) -> Result<JsExecutionResult, JsExecutionResult> {
let result = execute_javascript_code(code).await;
Expand Down Expand Up @@ -39,3 +91,151 @@ pub fn get_app_info() -> AppInfo {
author: "ExecuteJS Team".to_string(),
}
}

// JavaScript 코드를 oxlint로 린트하고 결과를 반환
#[tauri::command]
pub async fn lint_code(code: String) -> Result<Vec<LintResult>, String> {
use std::io::Write;
use tempfile::NamedTempFile;

// 임시 파일 생성
let mut temp_file = NamedTempFile::with_suffix(".js")
.map_err(|e| format!("Failed to create temp file: {}", e))?;

temp_file
.write_all(code.as_bytes())
.map_err(|e| format!("Failed to write to temp file: {}", e))?;

let temp_path = temp_file.path().to_str().ok_or("Invalid temp path")?;

// 프로젝트 루트 경로 찾기 (현재 실행 파일 위치 기준)
let current_exe =
std::env::current_exe().map_err(|e| format!("Failed to get current exe path: {}", e))?;

// Tauri 앱의 경우: target/debug/executeJS 또는 target/release/executeJS
// 프로젝트 루트는 3단계 위 (target/debug 또는 target/release)
let mut project_root = current_exe
.parent() // target/debug 또는 target/release
.and_then(|p| p.parent()) // target
.and_then(|p| p.parent()) // 프로젝트 루트
.ok_or("Failed to find project root")?;

// 개발 모드에서는 src-tauri가 있으므로 한 단계 더 올라가야 함
let src_tauri_path = project_root.join("apps/executeJS/src-tauri");
if src_tauri_path.exists() {
project_root = src_tauri_path
.parent()
.and_then(|p| p.parent())
.ok_or("Failed to find project root")?;
}

// oxlint 경로: 프로젝트 루트의 node_modules/.bin/oxlint
let oxlint_path = project_root
.join("node_modules")
.join(".bin")
.join("oxlint");

// oxlint 실행 (로컬 설치된 버전 사용)
let output = if oxlint_path.exists() {
// 로컬 설치된 oxlint 사용
Command::new(oxlint_path)
.arg("--format")
.arg("json")
.arg(temp_path)
.output()
.map_err(|e| format!("Failed to execute oxlint: {}", e))?
} else {
// fallback: pnpm exec oxlint (pnpm이 설치되어 있다면)
Command::new("pnpm")
.arg("exec")
.arg("oxlint")
.arg("--format")
.arg("json")
.arg(temp_path)
.current_dir(project_root)
.output()
.map_err(|e| format!("Failed to execute oxlint: {}", e))?
};

drop(temp_file);

// oxlint 출력 파싱
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

let results = parse_oxlint_output(&stdout, &stderr);

Ok(results)
}

// oxlint의 JSON 출력을 파싱하여 LintResult 벡터로 변환
fn parse_oxlint_output(stdout: &str, stderr: &str) -> Vec<LintResult> {
let output_text = if stdout.trim().is_empty() {
stderr
} else {
stdout
};

// JSON 형식으로 직접 deserialize
match serde_json::from_str::<OxlintOutput>(output_text) {
Ok(oxlint_output) => {
let mut results = Vec::new();

for diagnostic in oxlint_output.diagnostics {
// labels의 첫 번째 항목에서 위치 정보 가져오기
if let Some(label) = diagnostic.labels.first() {
let span = &label.span;
let line = span.line;
let column = span.column.max(1);
let end_column = if span.length > 0 {
column + span.length
} else {
// length가 없을 경우 최소값 사용
column + 1
};

// code에서 rule_id 추출
// 예: "eslint(no-unused-vars)" -> "no-unused-vars"
let rule_id = if diagnostic.code.starts_with("eslint(") {
diagnostic
.code
.strip_prefix("eslint(")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or("unknown")
.to_string()
} else {
diagnostic.code.clone()
};

// severity 변환
let severity = match diagnostic.severity.as_str() {
"error" => LintSeverity::Error,
"warning" => LintSeverity::Warning,
"info" => LintSeverity::Info,
"hint" => LintSeverity::Hint,
_ => LintSeverity::Warning, // 기본값
};

results.push(LintResult {
line,
column,
end_line: line,
end_column,
message: diagnostic.message,
severity,
rule_id,
});
}
}

results
}
Err(e) => {
// 에러 로깅 추가
eprintln!("Failed to parse oxlint output: {}", e);
eprintln!("stdout: {}", stdout);
eprintln!("stderr: {}", stderr);
Vec::new()
}
}
}
6 changes: 5 additions & 1 deletion apps/executeJS/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ pub fn run() {
tracing::info!("ExecuteJS 애플리케이션이 종료됩니다.");
}
})
.invoke_handler(tauri::generate_handler![execute_js, get_app_info])
.invoke_handler(tauri::generate_handler![
execute_js,
get_app_info,
lint_code
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Expand Down
12 changes: 12 additions & 0 deletions apps/executeJS/src/shared/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ export interface OutputPanelProps {
result: JsExecutionResult | null;
isExecuting: boolean;
}

export type LintSeverity = 'error' | 'warning' | 'info' | 'hint';

export interface LintResult {
line: number;
column: number;
end_line: number;
end_column: number;
message: string;
severity: LintSeverity;
rule_id: string;
}
Loading