From 811ac998ef2c5a45faf71b06f24e385228c0041f Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 19:15:16 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat(deno-runtime):=20npm=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20import=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - npm 레지스트리에서 패키지 다운로드 기능 구현 - NpmResolver: npm 패키지 다운로드, 압축 해제, 캐시 관리 - NpmModuleLoader: npm: 프로토콜을 통한 패키지 로드 지원 - ES 모듈 import 문 실행 지원 (load_main_es_module_from_code 사용) - 패키지 다운로드 과정 추적을 위한 로그 추가 - 의존성 추가: reqwest, tar, flate2, serde, serde_json, dirs, futures --- .cursorrules | 3 +- Cargo.lock | 88 ++++++++- crates/deno-runtime/Cargo.toml | 17 ++ crates/deno-runtime/src/lib.rs | 251 +++++++++++++++++++++++- crates/deno-runtime/src/npm_resolver.rs | 237 ++++++++++++++++++++++ 5 files changed, 580 insertions(+), 16 deletions(-) create mode 100644 crates/deno-runtime/src/npm_resolver.rs diff --git a/.cursorrules b/.cursorrules index 9cd4dc2..0c34212 100644 --- a/.cursorrules +++ b/.cursorrules @@ -19,7 +19,7 @@ - **Backend**: Rust, Tauri 2.0 - **JavaScript Engine**: Deno Core 0.323 (V8 기반) - **Package Manager**: pnpm -- **Linting**: oxlint (JavaScript), clippy (Rust) +- **Linting**: oxlint (JavaScript) - **Formatting**: Prettier (JavaScript), rustfmt (Rust) - **Documentation**: RSPress @@ -155,7 +155,6 @@ chore: 빌드 설정 변경 - [ ] Prettier 포맷팅 적용 - [ ] TypeScript 타입 체크 통과 - [ ] Vitest 테스트 통과 -- [ ] Rust clippy 통과 - [ ] rustfmt 포맷팅 적용 - [ ] cargo test 통과 diff --git a/Cargo.lock b/Cargo.lock index 9142d48..137c2b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,6 +1016,13 @@ version = "0.1.0" dependencies = [ "anyhow", "deno_core", + "dirs 5.0.1", + "flate2", + "futures", + "reqwest", + "serde", + "serde_json", + "tar", "tokio", "tracing", ] @@ -1163,13 +1170,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1180,7 +1208,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -1482,6 +1510,18 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -2753,6 +2793,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", + "redox_syscall", ] [[package]] @@ -4247,6 +4288,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4326,10 +4378,12 @@ dependencies = [ "http-body-util", "hyper 1.7.0", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -4340,6 +4394,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower 0.5.2", @@ -5222,6 +5277,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5237,7 +5303,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -5288,7 +5354,7 @@ checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -6132,7 +6198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2 0.6.3", @@ -7279,7 +7345,7 @@ dependencies = [ "block2 0.6.2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", @@ -7362,6 +7428,16 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.2", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/crates/deno-runtime/Cargo.toml b/crates/deno-runtime/Cargo.toml index 9895d51..6576d3c 100644 --- a/crates/deno-runtime/Cargo.toml +++ b/crates/deno-runtime/Cargo.toml @@ -16,5 +16,22 @@ tracing.workspace = true # Deno Core dependencies deno_core = "0.323" +# HTTP 클라이언트 (npm 레지스트리 API) +reqwest = { version = "0.12", features = ["rustls-tls", "json"] } + +# Tarball 압축 해제 +tar = "0.4" +flate2 = "1.0" + +# JSON 파싱 (npm 레지스트리 응답) +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# 파일 경로 처리 +dirs = "5.0" + +# Future 유틸리티 +futures = "0.3" + [dev-dependencies] tokio.workspace = true diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 8167c33..6cacb5a 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -1,11 +1,22 @@ +use anyhow::Error as AnyhowError; use anyhow::Result; +use deno_core::error::type_error; use deno_core::error::AnyError; -use deno_core::{extension, op2, FsModuleLoader, JsRuntime, RuntimeOptions}; +use deno_core::{ + extension, op2, FastString, FsModuleLoader, JsRuntime, ModuleLoadResponse, ModuleLoader, + ModuleSource, ModuleSourceCode, ModuleSpecifier, ModuleType, RequestedModuleType, + ResolutionKind, RuntimeOptions, +}; +use futures::FutureExt; use std::collections::VecDeque; +use std::fs; use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; +mod npm_resolver; +pub use npm_resolver::NpmResolver; + /// JavaScript 실행 결과를 저장하는 구조체 #[derive(Debug, Clone)] pub struct ExecutionOutput { @@ -99,6 +110,175 @@ extension!( ops = [op_console_log, op_alert, op_custom_print], ); +/// npm 패키지를 지원하는 모듈 로더 +pub struct NpmModuleLoader { + fs_loader: FsModuleLoader, + npm_resolver: Arc>, +} + +impl NpmModuleLoader { + pub fn new() -> Result { + Ok(Self { + fs_loader: FsModuleLoader, + npm_resolver: Arc::new(Mutex::new(NpmResolver::new()?)), + }) + } +} + +impl ModuleLoader for NpmModuleLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + kind: ResolutionKind, + ) -> Result { + eprintln!( + "[NpmModuleLoader::resolve] specifier: {}, referrer: {}, kind: {:?}", + specifier, referrer, kind + ); + + // npm: 프로토콜 처리 + if specifier.starts_with("npm:") { + eprintln!( + "[NpmModuleLoader::resolve] npm: 프로토콜 감지: {}", + specifier + ); + // npm: 프로토콜을 그대로 유지하여 load에서 처리 + ModuleSpecifier::parse(specifier).map_err(|e| { + eprintln!("[NpmModuleLoader::resolve] 모듈 스펙 해석 실패: {}", e); + let msg = format!("모듈 스펙 해석 실패: {}", e); + type_error(msg).into() + }) + } else { + eprintln!("[NpmModuleLoader::resolve] 일반 파일 시스템 모듈로 처리"); + // 일반 파일 시스템 모듈 + self.fs_loader.resolve(specifier, referrer, kind) + } + } + + fn load( + &self, + module_specifier: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, + is_dyn_import: bool, + requested_module_type: RequestedModuleType, + ) -> ModuleLoadResponse { + let specifier_str = module_specifier.as_str(); + let npm_resolver = self.npm_resolver.clone(); + let specifier = module_specifier.clone(); + + eprintln!( + "[NpmModuleLoader::load] specifier: {}, is_dyn_import: {}", + specifier_str, is_dyn_import + ); + + // npm: 프로토콜 처리 + if specifier_str.starts_with("npm:") { + eprintln!( + "[NpmModuleLoader::load] npm: 프로토콜 감지, 패키지 다운로드 시작: {}", + specifier_str + ); + let package_spec = &specifier_str[4..]; + + // 패키지명과 버전 파싱 + let (package_name, version) = if let Some(at_pos) = package_spec.rfind('@') { + // 스코프 패키지 처리 (@scope/package@version) + if package_spec.starts_with('@') { + // @scope/package@version 형식 + let after_first_at = &package_spec[1..]; + if let Some(second_at_pos) = after_first_at.rfind('@') { + let scope_and_name = &package_spec[..=second_at_pos]; + let version = &package_spec[second_at_pos + 1..]; + (scope_and_name.to_string(), Some(version.to_string())) + } else { + // @scope/package (버전 없음) + (package_spec.to_string(), None) + } + } else { + // package@version + let (name, version) = package_spec.split_at(at_pos); + (name.to_string(), Some(version[1..].to_string())) + } + } else { + (package_spec.to_string(), None) + }; + + // 비동기 로드 + // 필요한 데이터를 먼저 복사하고 락 해제 + let cache_dir = npm_resolver.lock().unwrap().cache_dir().to_path_buf(); + let registry_url = npm_resolver.lock().unwrap().registry_url().to_string(); + + let fut = async move { + eprintln!( + "[NpmModuleLoader::load] 패키지명: {}, 버전: {:?}", + package_name, version + ); + + // 패키지 다운로드 및 설치 (독립적인 리졸버 생성) + eprintln!("[NpmModuleLoader::load] NpmResolver 생성 중..."); + let resolver = + NpmResolver::with_cache_dir(cache_dir, registry_url).map_err(|e| { + eprintln!("[NpmModuleLoader::load] 리졸버 생성 실패: {}", e); + let msg = format!("리졸버 생성 실패: {}", e); + type_error(msg) + })?; + eprintln!("[NpmModuleLoader::load] 리졸버 생성 완료"); + + eprintln!( + "[NpmModuleLoader::load] install_package 호출: {}@{:?}", + package_name, + version.as_deref() + ); + let package_dir = resolver + .install_package(&package_name, version.as_deref()) + .await + .map_err(|e| { + eprintln!("[NpmModuleLoader::load] npm 패키지 다운로드 실패: {}", e); + let msg = format!("npm 패키지 다운로드 실패: {}", e); + type_error(msg) + })?; + eprintln!( + "[NpmModuleLoader::load] 패키지 다운로드 완료: {:?}", + package_dir + ); + + // 진입점 찾기 + let entry_point = resolver.find_entry_point(&package_dir).map_err(|e| { + let msg = format!("진입점 찾기 실패: {}", e); + type_error(msg) + })?; + + // 파일 읽기 + let code = fs::read_to_string(&entry_point).map_err(|e| { + let msg = format!("파일 읽기 실패: {}", e); + type_error(msg) + })?; + + // ModuleSource 생성 + let module_code = ModuleSourceCode::String(FastString::from(code)); + let module_source = ModuleSource::new( + ModuleType::JavaScript, + module_code, + &specifier, + None, // code_cache + ); + + Ok(module_source) + }; + + ModuleLoadResponse::Async(fut.boxed()) + } else { + // 일반 파일 시스템 모듈 + self.fs_loader.load( + module_specifier, + maybe_referrer, + is_dyn_import, + requested_module_type, + ) + } + } +} + /// JavaScript 실행기 (Deno Core 기반) pub struct DenoExecutor { output_buffer: Arc>, @@ -133,9 +313,19 @@ impl DenoExecutor { // 별도 스레드에서 Deno Core 실행 (Send 트레이트 문제 해결) let result = tokio::task::spawn_blocking(move || { + // 커스텀 모듈 로더 생성 (npm 지원) + let module_loader = match NpmModuleLoader::new() { + Ok(loader) => Rc::new(loader) as Rc, + Err(e) => { + // npm 리졸버 생성 실패 시 기본 로더 사용 + eprintln!("npm 모듈 로더 초기화 실패 (기본 로더 사용): {}", e); + Rc::new(FsModuleLoader) as Rc + } + }; + // JsRuntime 생성 let mut js_runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(Rc::new(FsModuleLoader)), + module_loader: Some(module_loader), extensions: vec![executejs_runtime::init_ops()], ..Default::default() }); @@ -147,14 +337,59 @@ impl DenoExecutor { } // 코드 실행 - let result = js_runtime.execute_script("[executejs:user_code]", code)?; - - // 이벤트 루프 실행 (Promise 처리) - 블로킹 방식으로 변경 + eprintln!( + "[DenoExecutor] 코드 실행 시작, 코드 길이: {} bytes", + code.len() + ); + eprintln!( + "[DenoExecutor] 코드 내용 (처음 200자): {}", + &code.chars().take(200).collect::() + ); + + // ES 모듈 import 구문이 있는지 확인 + let has_import = code.contains("import ") || code.contains("export "); + eprintln!("[DenoExecutor] ES 모듈 구문 감지: {}", has_import); + + // 이벤트 루프 실행을 위한 런타임 핸들 let rt = tokio::runtime::Handle::current(); - rt.block_on(async { js_runtime.run_event_loop(Default::default()).await })?; - // 결과 처리 - let _ = result; + if has_import { + // ES 모듈로 실행 + eprintln!("[DenoExecutor] ES 모듈로 실행 시도..."); + let specifier = ModuleSpecifier::parse("file:///executejs/user_code.mjs") + .map_err(|e| anyhow::anyhow!("스펙 해석 실패: {}", e))?; + + eprintln!( + "[DenoExecutor] load_main_es_module_from_code 호출: {}", + specifier + ); + let module_id = rt + .block_on(async { + js_runtime + .load_main_es_module_from_code(&specifier, code.clone()) + .await + }) + .map_err(|e| anyhow::anyhow!("모듈 로드 실패: {}", e))?; + + eprintln!("[DenoExecutor] 모듈 로드 완료, ModuleId: {}", module_id); + + // 모듈 평가 (비동기) + eprintln!("[DenoExecutor] mod_evaluate 호출..."); + rt.block_on(async { js_runtime.mod_evaluate(module_id).await }) + .map_err(|e| anyhow::anyhow!("모듈 평가 실패: {}", e))?; + + eprintln!("[DenoExecutor] mod_evaluate 완료"); + } else { + // 일반 스크립트로 실행 + eprintln!("[DenoExecutor] 일반 스크립트로 실행..."); + let _result = js_runtime.execute_script("[executejs:user_code]", code)?; + eprintln!("[DenoExecutor] execute_script 완료"); + } + + // 이벤트 루프 실행 (Promise 처리 및 모듈 로딩 완료 대기) + eprintln!("[DenoExecutor] 이벤트 루프 실행 시작..."); + rt.block_on(async { js_runtime.run_event_loop(Default::default()).await })?; + eprintln!("[DenoExecutor] 이벤트 루프 완료"); // 출력 버퍼에서 결과 가져오기 let output = output_buffer.lock().unwrap(); diff --git a/crates/deno-runtime/src/npm_resolver.rs b/crates/deno-runtime/src/npm_resolver.rs new file mode 100644 index 0000000..c5439c5 --- /dev/null +++ b/crates/deno-runtime/src/npm_resolver.rs @@ -0,0 +1,237 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; + +/// npm 레지스트리 메타데이터 응답 +#[derive(Debug, Deserialize)] +struct NpmRegistryResponse { + #[serde(rename = "dist-tags")] + dist_tags: DistTags, + versions: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +struct DistTags { + latest: String, +} + +/// 패키지 버전 메타데이터 +#[derive(Debug, Deserialize)] +struct PackageVersion { + version: String, + dist: Dist, +} + +#[derive(Debug, Deserialize)] +struct Dist { + tarball: String, +} + +/// npm 패키지 리졸버 +pub struct NpmResolver { + cache_dir: PathBuf, + registry_url: String, +} + +impl NpmResolver { + pub fn new() -> Result { + // 캐시 디렉토리 설정 (OS별 기본 경로) + let cache_dir = dirs::cache_dir() + .context("캐시 디렉토리를 찾을 수 없습니다")? + .join("executejs") + .join("npm"); + + // 캐시 디렉토리 생성 + fs::create_dir_all(&cache_dir).context("캐시 디렉토리를 생성할 수 없습니다")?; + + Ok(Self { + cache_dir, + registry_url: "https://registry.npmjs.org".to_string(), + }) + } + + /// 캐시 디렉토리와 레지스트리 URL을 지정하여 생성 + pub fn with_cache_dir(cache_dir: PathBuf, registry_url: String) -> Result { + // 캐시 디렉토리 생성 + fs::create_dir_all(&cache_dir).context("캐시 디렉토리를 생성할 수 없습니다")?; + + Ok(Self { + cache_dir, + registry_url, + }) + } + + /// 캐시 디렉토리 경로 반환 + pub fn cache_dir(&self) -> &Path { + &self.cache_dir + } + + /// 레지스트리 URL 반환 + pub fn registry_url(&self) -> &str { + &self.registry_url + } + + /// 패키지 다운로드 및 설치 + pub async fn install_package( + &self, + package_name: &str, + version: Option<&str>, + ) -> Result { + eprintln!( + "[NpmResolver::install_package] 시작: package_name={}, version={:?}", + package_name, version + ); + + // 패키지 버전 결정 + let version = match version { + Some(v) => { + eprintln!("[NpmResolver::install_package] 버전 지정됨: {}", v); + v.to_string() + } + None => { + eprintln!("[NpmResolver::install_package] 최신 버전 조회 중..."); + let latest = self.get_latest_version(package_name).await?; + eprintln!("[NpmResolver::install_package] 최신 버전: {}", latest); + latest + } + }; + + // 캐시 경로 + let package_dir = self.cache_dir.join(package_name).join(&version); + eprintln!( + "[NpmResolver::install_package] 캐시 경로: {:?}", + package_dir + ); + + // 이미 설치되어 있으면 스킵 + if package_dir.exists() { + eprintln!("[NpmResolver::install_package] 캐시 디렉토리 존재 확인 중..."); + // package.json이 존재하는지 확인 + let package_json_path = package_dir.join("package").join("package.json"); + if package_json_path.exists() { + eprintln!( + "[NpmResolver::install_package] 캐시된 패키지 사용: {:?}", + package_dir + ); + return Ok(package_dir); + } + eprintln!("[NpmResolver::install_package] package.json 없음, 재다운로드 필요"); + } + + // 패키지 메타데이터 가져오기 + eprintln!("[NpmResolver::install_package] tarball URL 조회 중..."); + let tarball_url = self.get_tarball_url(package_name, &version).await?; + eprintln!( + "[NpmResolver::install_package] tarball URL: {}", + tarball_url + ); + + // tarball 다운로드 + eprintln!("[NpmResolver::install_package] tarball 다운로드 시작..."); + let tarball_data = self.download_tarball(&tarball_url).await?; + eprintln!( + "[NpmResolver::install_package] tarball 다운로드 완료: {} bytes", + tarball_data.len() + ); + + // 기존 디렉토리 삭제 후 재생성 + if package_dir.exists() { + fs::remove_dir_all(&package_dir) + .context("기존 패키지 디렉토리를 삭제할 수 없습니다")?; + } + fs::create_dir_all(&package_dir).context("패키지 디렉토리를 생성할 수 없습니다")?; + + // 압축 해제 + eprintln!("[NpmResolver::install_package] tarball 압축 해제 중..."); + self.extract_tarball(&tarball_data, &package_dir)?; + eprintln!("[NpmResolver::install_package] 압축 해제 완료"); + + eprintln!( + "[NpmResolver::install_package] 패키지 설치 완료: {:?}", + package_dir + ); + Ok(package_dir) + } + + /// 최신 버전 가져오기 + async fn get_latest_version(&self, package_name: &str) -> Result { + let url = format!("{}/{}", self.registry_url, package_name); + let response = reqwest::get(&url).await?; + let metadata: NpmRegistryResponse = response.json().await?; + Ok(metadata.dist_tags.latest) + } + + /// tarball URL 가져오기 + async fn get_tarball_url(&self, package_name: &str, version: &str) -> Result { + let url = format!("{}/{}", self.registry_url, package_name); + let response = reqwest::get(&url).await?; + let metadata: serde_json::Value = response.json().await?; + + let version_data = metadata["versions"][version] + .as_object() + .context("패키지 버전을 찾을 수 없습니다")?; + + let tarball_url = version_data["dist"]["tarball"] + .as_str() + .context("tarball URL을 찾을 수 없습니다")?; + + Ok(tarball_url.to_string()) + } + + /// tarball 다운로드 + async fn download_tarball(&self, url: &str) -> Result> { + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + Ok(bytes.to_vec()) + } + + /// tarball 압축 해제 + fn extract_tarball(&self, data: &[u8], target_dir: &Path) -> Result<()> { + // 디렉토리 생성 + fs::create_dir_all(target_dir).context("타겟 디렉토리를 생성할 수 없습니다")?; + + // gzip 압축 해제 + let mut decoder = flate2::read::GzDecoder::new(data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + + // tar 압축 해제 + let mut archive = tar::Archive::new(&decompressed[..]); + archive.unpack(target_dir)?; + + Ok(()) + } + + /// 패키지의 진입점 파일 찾기 (package.json의 main 필드) + pub fn find_entry_point(&self, package_dir: &Path) -> Result { + let package_json_path = package_dir.join("package").join("package.json"); + + let package_json_content = + fs::read_to_string(&package_json_path).context("package.json을 읽을 수 없습니다")?; + + let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?; + + // main, module, exports 우선순위로 찾기 + let entry_point = package_json["main"] + .as_str() + .or_else(|| package_json["module"].as_str()) + .or_else(|| { + // exports 필드에서 "." 경로 찾기 + package_json["exports"] + .as_object() + .and_then(|e| e.get(".")) + .and_then(|e| { + e.as_str().or_else(|| { + e.as_object() + .and_then(|e| e.get("import").or_else(|| e.get("require"))) + .and_then(|e| e.as_str()) + }) + }) + }) + .unwrap_or("index.js"); // 기본값 + + Ok(package_dir.join("package").join(entry_point)) + } +} From f338466e94a202c1869dbf130c61ab1974d6ef68 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 19:20:19 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor(deno-runtime):=20ES=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=8B=A4=ED=96=89=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EB=94=94=EB=B2=84=EA=B9=85=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ES 모듈 import 구문 감지 시 load_main_es_module_from_code 사용 - npm 패키지 다운로드 과정 상세 로그 추가 - 진입점 찾기, 파일 읽기, ModuleSource 생성 단계 로그 - NpmResolver::find_entry_point 상세 로그 - 모듈 로더 resolve/load 메서드 호출 추적 로그 추가 --- crates/deno-runtime/src/lib.rs | 11 +++++++++++ crates/deno-runtime/src/npm_resolver.rs | 20 +++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 6cacb5a..59ac624 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -243,18 +243,28 @@ impl ModuleLoader for NpmModuleLoader { ); // 진입점 찾기 + eprintln!("[NpmModuleLoader::load] 진입점 찾기 시작..."); let entry_point = resolver.find_entry_point(&package_dir).map_err(|e| { + eprintln!("[NpmModuleLoader::load] 진입점 찾기 실패: {}", e); let msg = format!("진입점 찾기 실패: {}", e); type_error(msg) })?; + eprintln!("[NpmModuleLoader::load] 진입점: {:?}", entry_point); // 파일 읽기 + eprintln!("[NpmModuleLoader::load] 파일 읽기 시작..."); let code = fs::read_to_string(&entry_point).map_err(|e| { + eprintln!("[NpmModuleLoader::load] 파일 읽기 실패: {}", e); let msg = format!("파일 읽기 실패: {}", e); type_error(msg) })?; + eprintln!( + "[NpmModuleLoader::load] 파일 읽기 완료, 코드 길이: {} bytes", + code.len() + ); // ModuleSource 생성 + eprintln!("[NpmModuleLoader::load] ModuleSource 생성 중..."); let module_code = ModuleSourceCode::String(FastString::from(code)); let module_source = ModuleSource::new( ModuleType::JavaScript, @@ -262,6 +272,7 @@ impl ModuleLoader for NpmModuleLoader { &specifier, None, // code_cache ); + eprintln!("[NpmModuleLoader::load] ModuleSource 생성 완료"); Ok(module_source) }; diff --git a/crates/deno-runtime/src/npm_resolver.rs b/crates/deno-runtime/src/npm_resolver.rs index c5439c5..2eb12e5 100644 --- a/crates/deno-runtime/src/npm_resolver.rs +++ b/crates/deno-runtime/src/npm_resolver.rs @@ -207,11 +207,17 @@ impl NpmResolver { /// 패키지의 진입점 파일 찾기 (package.json의 main 필드) pub fn find_entry_point(&self, package_dir: &Path) -> Result { let package_json_path = package_dir.join("package").join("package.json"); + eprintln!( + "[NpmResolver::find_entry_point] package.json 경로: {:?}", + package_json_path + ); let package_json_content = fs::read_to_string(&package_json_path).context("package.json을 읽을 수 없습니다")?; + eprintln!("[NpmResolver::find_entry_point] package.json 읽기 완료"); let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?; + eprintln!("[NpmResolver::find_entry_point] package.json 파싱 완료"); // main, module, exports 우선순위로 찾기 let entry_point = package_json["main"] @@ -232,6 +238,18 @@ impl NpmResolver { }) .unwrap_or("index.js"); // 기본값 - Ok(package_dir.join("package").join(entry_point)) + eprintln!( + "[NpmResolver::find_entry_point] 진입점 이름: {}", + entry_point + ); + + let full_path = package_dir.join("package").join(entry_point); + eprintln!("[NpmResolver::find_entry_point] 전체 경로: {:?}", full_path); + eprintln!( + "[NpmResolver::find_entry_point] 파일 존재 여부: {}", + full_path.exists() + ); + + Ok(full_path) } } From 5fc8e7a597086ae0d0b6f9c72b99c41a2cbea362 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 19:28:17 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix(deno-runtime):=20npm=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=82=B4=EB=B6=80=20=EC=83=81=EB=8C=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20import=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - npm: URL과 실제 파일 경로 매핑 구조 추가 - resolve 시 npm: referrer를 실제 file:// URL로 변환 - load 시 npm 패키지 진입점 경로를 매핑에 저장 - npm 패키지 내부 상대 경로(./add.js 등) import 정상 처리 --- crates/deno-runtime/src/lib.rs | 70 ++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 59ac624..44331f8 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -8,8 +8,9 @@ use deno_core::{ ResolutionKind, RuntimeOptions, }; use futures::FutureExt; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::fs; +use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; @@ -114,6 +115,8 @@ extension!( pub struct NpmModuleLoader { fs_loader: FsModuleLoader, npm_resolver: Arc>, + /// npm: URL과 실제 파일 경로 매핑 + npm_path_map: Arc>>, } impl NpmModuleLoader { @@ -121,6 +124,7 @@ impl NpmModuleLoader { Ok(Self { fs_loader: FsModuleLoader, npm_resolver: Arc::new(Mutex::new(NpmResolver::new()?)), + npm_path_map: Arc::new(Mutex::new(HashMap::new())), }) } } @@ -150,9 +154,57 @@ impl ModuleLoader for NpmModuleLoader { type_error(msg).into() }) } else { - eprintln!("[NpmModuleLoader::resolve] 일반 파일 시스템 모듈로 처리"); + // npm 패키지 내부의 상대 경로 처리 + // referrer가 npm: 프로토콜이면 실제 파일 경로로 변환 + let actual_referrer = if referrer.starts_with("npm:") { + eprintln!( + "[NpmModuleLoader::resolve] npm 패키지 내부 상대 경로 감지: {} (referrer: {})", + specifier, referrer + ); + + // npm: URL과 실제 파일 경로 매핑에서 찾기 + let path_map = self.npm_path_map.lock().unwrap(); + if let Some(actual_path) = path_map.get(referrer) { + // 실제 파일 경로를 file:// URL로 변환 + // ModuleSpecifier를 사용하여 올바른 URL 형식으로 변환 + let file_url = match ModuleSpecifier::from_file_path(actual_path) { + Ok(url) => { + eprintln!( + "[NpmModuleLoader::resolve] 실제 경로로 변환: {} -> {}", + referrer, + url.as_str() + ); + url.as_str().to_string() + } + Err(e) => { + eprintln!( + "[NpmModuleLoader::resolve] 파일 경로를 URL로 변환 실패: {:?}", + e + ); + // 폴백: file:// 절대 경로 형식 사용 + let path_str = actual_path.to_string_lossy(); + format!("file://{}", path_str) + } + }; + file_url + } else { + eprintln!( + "[NpmModuleLoader::resolve] 경로 매핑을 찾을 수 없음: {}", + referrer + ); + // 매핑이 없으면 원래 referrer 사용 (에러 발생 가능) + referrer.to_string() + } + } else { + referrer.to_string() + }; + + eprintln!( + "[NpmModuleLoader::resolve] 일반 파일 시스템 모듈로 처리 (referrer: {})", + actual_referrer + ); // 일반 파일 시스템 모듈 - self.fs_loader.resolve(specifier, referrer, kind) + self.fs_loader.resolve(specifier, &actual_referrer, kind) } } @@ -165,6 +217,7 @@ impl ModuleLoader for NpmModuleLoader { ) -> ModuleLoadResponse { let specifier_str = module_specifier.as_str(); let npm_resolver = self.npm_resolver.clone(); + let npm_path_map = self.npm_path_map.clone(); let specifier = module_specifier.clone(); eprintln!( @@ -251,6 +304,17 @@ impl ModuleLoader for NpmModuleLoader { })?; eprintln!("[NpmModuleLoader::load] 진입점: {:?}", entry_point); + // npm: URL과 실제 파일 경로 매핑 저장 + let specifier_str_for_map = specifier.as_str().to_string(); + { + let mut path_map = npm_path_map.lock().unwrap(); + path_map.insert(specifier_str_for_map.clone(), entry_point.clone()); + eprintln!( + "[NpmModuleLoader::load] 경로 매핑 저장: {} -> {:?}", + specifier_str_for_map, entry_point + ); + } + // 파일 읽기 eprintln!("[NpmModuleLoader::load] 파일 읽기 시작..."); let code = fs::read_to_string(&entry_point).map_err(|e| { From 9592cee2c0882eea7d79941f4dd95e7758cb93e2 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 19:32:03 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat(deno-runtime):=20npm=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20TypeScript=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json의 types/typings 필드 확인 기능 추가 - @types 패키지 자동 검색 로그 추가 - 타입 정의 파일 경로 로깅 - 파일 확장자 기반 ModuleType 결정 (TypeScript 파일 감지) - find_type_definitions 메서드 추가 --- crates/deno-runtime/src/lib.rs | 41 +++++++++++++++- crates/deno-runtime/src/npm_resolver.rs | 63 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 44331f8..6ffbd64 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -304,6 +304,18 @@ impl ModuleLoader for NpmModuleLoader { })?; eprintln!("[NpmModuleLoader::load] 진입점: {:?}", entry_point); + // 타입 정의 파일 찾기 + eprintln!("[NpmModuleLoader::load] 타입 정의 파일 찾기 시작..."); + let type_def = resolver.find_type_definitions(&package_dir).unwrap_or(None); + if let Some(type_def_path) = &type_def { + eprintln!( + "[NpmModuleLoader::load] 타입 정의 파일 발견: {:?}", + type_def_path + ); + } else { + eprintln!("[NpmModuleLoader::load] 타입 정의 파일 없음"); + } + // npm: URL과 실제 파일 경로 매핑 저장 let specifier_str_for_map = specifier.as_str().to_string(); { @@ -327,11 +339,38 @@ impl ModuleLoader for NpmModuleLoader { code.len() ); + // 파일 확장자에 따라 ModuleType 결정 + // Deno Core는 TypeScript를 자동으로 처리하므로 JavaScript로 설정 + // 실제 TypeScript 파일은 런타임에서 자동 변환됨 + let file_ext = entry_point + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + + let module_type = match file_ext { + "ts" | "tsx" | "mts" | "cts" => { + eprintln!( + "[NpmModuleLoader::load] TypeScript 모듈로 감지 (확장자: {})", + file_ext + ); + // Deno Core는 TypeScript도 JavaScript로 처리 가능 + ModuleType::JavaScript + } + "jsx" => { + eprintln!("[NpmModuleLoader::load] JSX 모듈로 감지"); + ModuleType::JavaScript + } + _ => { + eprintln!("[NpmModuleLoader::load] JavaScript 모듈로 감지"); + ModuleType::JavaScript + } + }; + // ModuleSource 생성 eprintln!("[NpmModuleLoader::load] ModuleSource 생성 중..."); let module_code = ModuleSourceCode::String(FastString::from(code)); let module_source = ModuleSource::new( - ModuleType::JavaScript, + module_type, module_code, &specifier, None, // code_cache diff --git a/crates/deno-runtime/src/npm_resolver.rs b/crates/deno-runtime/src/npm_resolver.rs index 2eb12e5..f139135 100644 --- a/crates/deno-runtime/src/npm_resolver.rs +++ b/crates/deno-runtime/src/npm_resolver.rs @@ -252,4 +252,67 @@ impl NpmResolver { Ok(full_path) } + + /// 패키지의 타입 정의 파일 찾기 (package.json의 types 또는 typings 필드) + pub fn find_type_definitions(&self, package_dir: &Path) -> Result> { + let package_json_path = package_dir.join("package").join("package.json"); + eprintln!( + "[NpmResolver::find_type_definitions] package.json 경로: {:?}", + package_json_path + ); + + if !package_json_path.exists() { + eprintln!("[NpmResolver::find_type_definitions] package.json 없음"); + return Ok(None); + } + + let package_json_content = + fs::read_to_string(&package_json_path).context("package.json을 읽을 수 없습니다")?; + let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?; + + // types 또는 typings 필드 확인 + let type_def = package_json["types"] + .as_str() + .or_else(|| package_json["typings"].as_str()); + + if let Some(type_def_path) = type_def { + eprintln!( + "[NpmResolver::find_type_definitions] 타입 정의 경로 발견: {}", + type_def_path + ); + let full_path = package_dir.join("package").join(type_def_path); + eprintln!( + "[NpmResolver::find_type_definitions] 전체 경로: {:?}", + full_path + ); + eprintln!( + "[NpmResolver::find_type_definitions] 파일 존재 여부: {}", + full_path.exists() + ); + + if full_path.exists() { + Ok(Some(full_path)) + } else { + eprintln!("[NpmResolver::find_type_definitions] 타입 정의 파일이 존재하지 않음"); + Ok(None) + } + } else { + eprintln!("[NpmResolver::find_type_definitions] types/typings 필드 없음"); + // @types/ 패키지 자동 검색 (예: lodash -> @types/lodash) + let package_name = package_json["name"].as_str().unwrap_or(""); + + // 스코프가 없는 패키지만 @types 검색 + if !package_name.starts_with('@') && !package_name.is_empty() { + let types_package = format!("@types/{}", package_name); + eprintln!( + "[NpmResolver::find_type_definitions] @types 패키지 검색 시도: {}", + types_package + ); + // @types 패키지는 별도로 설치해야 하므로 여기서는 로그만 남김 + // 실제로는 npm: 프로토콜을 통해 로드 가능 + } + + Ok(None) + } + } } From 5b62df7884efc114bbedbf9cc24def2a5fe3fbc1 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 20:54:25 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EB=AA=85?= =?UTF-8?q?=ED=99=95=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/deno-runtime/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 6ffbd64..81805f0 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -471,7 +471,7 @@ impl DenoExecutor { // ES 모듈로 실행 eprintln!("[DenoExecutor] ES 모듈로 실행 시도..."); let specifier = ModuleSpecifier::parse("file:///executejs/user_code.mjs") - .map_err(|e| anyhow::anyhow!("스펙 해석 실패: {}", e))?; + .map_err(|e| anyhow::anyhow!("{}", e))?; eprintln!( "[DenoExecutor] load_main_es_module_from_code 호출: {}", @@ -483,14 +483,14 @@ impl DenoExecutor { .load_main_es_module_from_code(&specifier, code.clone()) .await }) - .map_err(|e| anyhow::anyhow!("모듈 로드 실패: {}", e))?; + .map_err(|e| anyhow::anyhow!("{}", e))?; eprintln!("[DenoExecutor] 모듈 로드 완료, ModuleId: {}", module_id); // 모듈 평가 (비동기) eprintln!("[DenoExecutor] mod_evaluate 호출..."); rt.block_on(async { js_runtime.mod_evaluate(module_id).await }) - .map_err(|e| anyhow::anyhow!("모듈 평가 실패: {}", e))?; + .map_err(|e| anyhow::anyhow!("{}", e))?; eprintln!("[DenoExecutor] mod_evaluate 완료"); } else { From 2cb97ed7c66ea71c39222f7d66a9a071876bc9e0 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:23:54 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix(deno-runtime):=20ESM=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=EC=88=9C=EC=9C=84=EB=A1=9C=20npm=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=A7=84=EC=9E=85=EC=A0=90=20=EC=B0=BE=EA=B8=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exports.import > module > exports.default > main 순서로 ESM 파일을 우선적으로 선택하도록 변경 --- crates/deno-runtime/src/npm_resolver.rs | 44 +++++++++++++++++++------ 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/crates/deno-runtime/src/npm_resolver.rs b/crates/deno-runtime/src/npm_resolver.rs index f139135..d10f408 100644 --- a/crates/deno-runtime/src/npm_resolver.rs +++ b/crates/deno-runtime/src/npm_resolver.rs @@ -204,7 +204,7 @@ impl NpmResolver { Ok(()) } - /// 패키지의 진입점 파일 찾기 (package.json의 main 필드) + /// 패키지의 진입점 파일 찾기 (ESM 우선: exports.import > module > main) pub fn find_entry_point(&self, package_dir: &Path) -> Result { let package_json_path = package_dir.join("package").join("package.json"); eprintln!( @@ -219,23 +219,47 @@ impl NpmResolver { let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?; eprintln!("[NpmResolver::find_entry_point] package.json 파싱 완료"); - // main, module, exports 우선순위로 찾기 - let entry_point = package_json["main"] - .as_str() - .or_else(|| package_json["module"].as_str()) + // ESM 우선순위로 찾기: exports.import > module > exports.default > main + let entry_point = package_json["exports"] + .as_object() + .and_then(|e| e.get(".")) + .and_then(|e| { + e.as_object().and_then(|e| { + // exports["."].import.default 우선 확인 (ESM) + e.get("import").and_then(|i| { + i.as_object() + .and_then(|i| i.get("default")) + .and_then(|d| d.as_str()) + .or_else(|| { + // exports["."].import가 직접 문자열인 경우 + i.as_str() + }) + }) + }) + }) + .or_else(|| package_json["module"].as_str()) // module 필드 (ESM) + .or_else(|| { + // exports["."]가 직접 문자열인 경우 + package_json["exports"] + .as_object() + .and_then(|e| e.get(".")) + .and_then(|e| e.as_str()) + }) .or_else(|| { - // exports 필드에서 "." 경로 찾기 + // exports["."].require (CommonJS, 폴백) package_json["exports"] .as_object() .and_then(|e| e.get(".")) .and_then(|e| { - e.as_str().or_else(|| { - e.as_object() - .and_then(|e| e.get("import").or_else(|| e.get("require"))) - .and_then(|e| e.as_str()) + e.as_object().and_then(|e| e.get("require")).and_then(|r| { + r.as_object() + .and_then(|r| r.get("default")) + .and_then(|d| d.as_str()) + .or_else(|| r.as_str()) }) }) }) + .or_else(|| package_json["main"].as_str()) // main (CommonJS, 최종 폴백) .unwrap_or("index.js"); // 기본값 eprintln!( From 1e7dd349646e3865ce6a2eeb17824fa02da73ee2 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:23:55 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat(deno-runtime):=20Node.js=20process?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80=EB=A1=9C=20npm=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=ED=98=B8=ED=99=98=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit immer 등 npm 패키지에서 사용하는 process.env.NODE_ENV 지원 --- crates/deno-runtime/src/bootstrap.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/deno-runtime/src/bootstrap.js b/crates/deno-runtime/src/bootstrap.js index 7300fa3..83d8ee2 100644 --- a/crates/deno-runtime/src/bootstrap.js +++ b/crates/deno-runtime/src/bootstrap.js @@ -147,3 +147,24 @@ if (typeof globalThis.module === 'undefined') { if (typeof globalThis.exports === 'undefined') { globalThis.exports = globalThis.module.exports; } + +// Node.js process 객체 정의 (npm 모듈 호환성) +if (typeof globalThis.process === 'undefined') { + globalThis.process = { + env: { + NODE_ENV: 'development', + }, + version: 'v20.0.0', + versions: { + node: '20.0.0', + v8: '10.2.0', + }, + platform: 'darwin', + arch: 'x64', + cwd: () => '/', + nextTick: (callback) => { + // setTimeout으로 시뮬레이션 + setTimeout(callback, 0); + }, + }; +} From dd63ee983faff4d1effbd0ebef823516d20f1330 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:24:04 +0900 Subject: [PATCH 08/13] =?UTF-8?q?docs:=20=EA=B0=9C=EB=B0=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 참조를 개발하기 섹션으로 이동 - 개발 가이드를 dev 폴더로 이동 - 가이드 섹션에 npm-modules 추가 --- docs/docs/dev/_meta.json | 2 + docs/docs/dev/api/commands.mdx | 38 ++++++ docs/docs/dev/development.mdx | 208 +++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 docs/docs/dev/_meta.json create mode 100644 docs/docs/dev/api/commands.mdx create mode 100644 docs/docs/dev/development.mdx diff --git a/docs/docs/dev/_meta.json b/docs/docs/dev/_meta.json new file mode 100644 index 0000000..fe17cc2 --- /dev/null +++ b/docs/docs/dev/_meta.json @@ -0,0 +1,2 @@ +["development", "api"] + diff --git a/docs/docs/dev/api/commands.mdx b/docs/docs/dev/api/commands.mdx new file mode 100644 index 0000000..0e694b7 --- /dev/null +++ b/docs/docs/dev/api/commands.mdx @@ -0,0 +1,38 @@ +# API 참조 + +ExecuteJS에서 사용할 수 있는 모든 Tauri 명령어를 확인하세요. + +## 기본 명령어 + +### execute_js + +자바스크립트에 실행을 요청하는 명령어입니다. + +```typescript +import { invoke } from '@tauri-apps/api/core'; + +const result = await invoke('execute_js', { + code: 'console.log("Hello, World!");', +}); +console.log(result); +``` + +**매개변수:** + +- `code` (string): 실행할 JavaScript 코드 + +**반환값:** + +- `JsExecutionResult`: 실행 결과 객체 + +## 에러 처리 + +모든 명령어는 Promise를 반환하며, 에러가 발생할 경우 적절한 에러 메시지와 함께 거부됩니다. + +```typescript +try { + const result = await invoke('execute_js', { code: 'invalid syntax' }); +} catch (error) { + console.error('Execution failed:', error); +} +``` diff --git a/docs/docs/dev/development.mdx b/docs/docs/dev/development.mdx new file mode 100644 index 0000000..4d87957 --- /dev/null +++ b/docs/docs/dev/development.mdx @@ -0,0 +1,208 @@ +# 개발 가이드 + +ExecuteJS 프로젝트의 개발 환경 설정과 기여 방법을 안내합니다. + +## 개발 환경 설정 + +### 1. 필수 도구 설치 + +```bash +# Node.js 22 LTS 설치 (nvm 사용 권장) +nvm install 22 +nvm use 22 + +# pnpm 설치 +npm install -g pnpm + +# Rust 설치 +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Tauri CLI 설치 +cargo install tauri-cli +``` + +### 2. 프로젝트 설정 + +```bash +# 의존성 설치 +pnpm install + +# 개발 서버 실행 +pnpm tauri:dev +``` + +## 개발 워크플로우 + +### 코드 품질 관리 + +```bash +# 린트 검사 +pnpm lint + +# 린트 자동 수정 +pnpm lint:fix + +# 포맷팅 +pnpm format + +# 포맷팅 검사 +pnpm format:check + +# 타입 검사 +pnpm type-check +``` + +### 테스트 + +```bash +# 프론트엔드 테스트 +pnpm test + +# Rust 테스트 +cargo test + +# 모든 테스트 +pnpm test --run +cargo test --all-targets +``` + +### 빌드 + +```bash +# 개발 빌드 +pnpm tauri:dev + +# 프로덕션 빌드 +pnpm tauri:build +``` + +## CI/CD + +프로젝트는 GitHub Actions를 통해 자동화된 품질 관리를 제공합니다: + +- **JavaScript/TypeScript Lint**: oxlint를 통한 코드 품질 검사 +- **Rust Lint**: rustfmt와 clippy를 통한 Rust 코드 검사 +- **Frontend Test**: vitest를 통한 프론트엔드 테스트 +- **Rust Test**: cargo test를 통한 Rust 테스트 +- **Build Check**: 전체 프로젝트 빌드 검증 + +## 기여하기 + +1. 이슈를 생성하거나 기존 이슈를 확인하세요. +2. 포크를 생성하고 새 브랜치를 만드세요. +3. 변경사항을 구현하고 테스트하세요. +4. 커밋 메시지를 명확하게 작성하세요. +5. Pull Request를 생성하세요. + +### 커밋 컨벤션 + +``` +type(scope): description + +feat: 새로운 기능 추가 +fix: 버그 수정 +docs: 문서 수정 +style: 코드 포맷팅 +refactor: 코드 리팩토링 +test: 테스트 추가/수정 +chore: 빌드 설정 변경 +``` + +## 아키텍처 + +### FSD (Feature-Sliced Design) 아키텍처 + +ExecuteJS 프론트엔드는 FSD 아키텍처를 사용하여 코드를 체계적으로 구성합니다. + +#### 폴더 구조 + +``` +src/ +├── app/ # 앱 초기화 및 프로바이더 +│ ├── index.tsx # 앱 루트 컴포넌트 +│ └── providers.tsx # 전역 프로바이더 (Legend State, Radix UI) +├── pages/ # 페이지 레벨 컴포넌트 +│ └── playground/ +│ └── PlaygroundPage.tsx # 플레이그라운드 페이지 +├── widgets/ # 복합 UI 블록 +│ ├── code-editor/ +│ │ └── CodeEditor.tsx # Monaco Editor 래퍼 +│ ├── output-panel/ +│ │ └── OutputPanel.tsx # 출력 결과 패널 +│ └── tab-bar/ +│ └── TabBar.tsx # 탭 관리 UI +├── features/ # 비즈니스 로직 기능 +│ ├── execute-code/ +│ │ ├── model.ts # 실행 상태 및 로직 +│ │ └── api.ts # Tauri command 호출 +│ └── manage-tabs/ +│ └── model.ts # 탭 관리 상태 (localStorage 연동) +├── shared/ # 공유 유틸리티 +│ ├── ui/ # 공통 UI 컴포넌트 +│ ├── lib/ # 유틸 함수 +│ └── types/ # 공통 타입 +└── main.tsx # Vite 엔트리 +``` + +#### 레이어별 역할 + +- **app/**: 앱 초기화, 전역 프로바이더 설정 +- **pages/**: 페이지 레벨 컴포넌트 (라우팅 단위) +- **widgets/**: 복합 UI 블록 (여러 features 조합) +- **features/**: 비즈니스 로직 기능 (도메인별) +- **shared/**: 공유 유틸리티 (재사용 가능) + +#### 의존성 규칙 + +- 상위 레이어는 하위 레이어를 import 가능 +- 같은 레이어 내에서는 import 금지 +- shared는 다른 모든 레이어에서 import 가능 + +### 프론트엔드 기술 스택 + +- **상태 관리**: Zustand (경량 상태 관리) +- **UI 라이브러리**: Radix UI + Tailwind CSS +- **코드 에디터**: Monaco Editor +- **레이아웃**: react-resizable-panels +- **빌드 도구**: Vite +- **테스팅**: Vitest + Testing Library +- **린팅**: oxlint + Prettier + +### 백엔드 (Rust + Tauri) + +- **프레임워크**: Tauri 2.0 +- **JavaScript 엔진**: Deno Core 0.323 (V8 기반) +- **npm 모듈 지원**: npm 레지스트리에서 패키지를 직접 다운로드 및 사용 +- **CommonJS 변환**: CommonJS 패키지를 ES 모듈로 자동 변환(지원 예정) +- **UMD 변환**: CommonJS 패키지를 ES 모듈로 자동 변환(지원 예정) +- **테스팅**: cargo test +- **린팅**: rustfmt +- **로깅**: tracing + +#### npm 모듈 로딩 아키텍처 + +ExecuteJS는 커스텀 `ModuleLoader`를 구현하여 npm 패키지를 지원합니다: + +1. **NpmModuleLoader**: `npm:` 프로토콜을 처리하는 모듈 로더 +2. **NpmResolver**: npm 레지스트리에서 패키지를 다운로드하고 캐시 +3. **CommonJS 변환**: `swc`를 통해 CommonJS를 ES 모듈로 변환(지원 예정) +4. **UMD 변환**: `swc`를 통해 UMD ES 모듈로 변환(지원 예정) + +``` +crates/deno-runtime/src/ +├── lib.rs # NpmModuleLoader 구현 +└── npm_resolver.rs # npm 패키지 다운로드 및 캐시 관리 +``` + +**주요 기능**: + +- npm 레지스트리 API를 통한 패키지 다운로드 +- 로컬 캐시를 통한 빠른 재사용 +- CommonJS, UMD → ES Module 자동 변환 (지원 예정) +- 패키지 내부 상대 경로 import 지원 + +### 모노레포 구조 + +- **pnpm 워크스페이스**: Node.js 패키지 관리 +- **Cargo 워크스페이스**: Rust 크레이트 관리 +- **조건부 CI**: 폴더별 변경사항에 따른 워크플로우 실행 From 178de31fe712ac27f9761eb1a7781087125b1aa0 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:24:05 +0900 Subject: [PATCH 09/13] =?UTF-8?q?docs:=20npm=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=99=88=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - npm 모듈 사용하기 가이드 추가 - getting-started에 npm 모듈 예제 추가 - 홈페이지에 npm 모듈 지원 및 CommonJS 변환 기능 추가 --- docs/docs/_meta.json | 6 +- docs/docs/guide/_meta.json | 6 +- docs/docs/guide/development.mdx | 183 ---------------------------- docs/docs/guide/getting-started.mdx | 15 ++- docs/docs/guide/npm-modules.mdx | 103 ++++++++++++++++ docs/docs/index.mdx | 4 +- 6 files changed, 127 insertions(+), 190 deletions(-) delete mode 100644 docs/docs/guide/development.mdx create mode 100644 docs/docs/guide/npm-modules.mdx diff --git a/docs/docs/_meta.json b/docs/docs/_meta.json index c6e6c85..66d092e 100644 --- a/docs/docs/_meta.json +++ b/docs/docs/_meta.json @@ -5,8 +5,8 @@ "activeMatch": "/guide/" }, { - "text": "API 참조", - "link": "/api/commands", - "activeMatch": "/api/" + "text": "개발하기", + "link": "/dev/development", + "activeMatch": "/dev/" } ] diff --git a/docs/docs/guide/_meta.json b/docs/docs/guide/_meta.json index f30fef7..209ca5f 100644 --- a/docs/docs/guide/_meta.json +++ b/docs/docs/guide/_meta.json @@ -1 +1,5 @@ -["getting-started", "development", "live-demo"] +[ + "getting-started", + "npm-modules", + "live-demo" +] diff --git a/docs/docs/guide/development.mdx b/docs/docs/guide/development.mdx deleted file mode 100644 index db81cc6..0000000 --- a/docs/docs/guide/development.mdx +++ /dev/null @@ -1,183 +0,0 @@ -# 개발 가이드 - -ExecuteJS 프로젝트의 개발 환경 설정과 기여 방법을 안내합니다. - -## 개발 환경 설정 - -### 1. 필수 도구 설치 - -```bash -# Node.js 22 LTS 설치 (nvm 사용 권장) -nvm install 22 -nvm use 22 - -# pnpm 설치 -npm install -g pnpm - -# Rust 설치 -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - -# Tauri CLI 설치 -cargo install tauri-cli -``` - -### 2. 프로젝트 설정 - -```bash -# 의존성 설치 -pnpm install - -# 개발 서버 실행 -pnpm tauri:dev -``` - -## 개발 워크플로우 - -### 코드 품질 관리 - -```bash -# 린트 검사 -pnpm lint - -# 린트 자동 수정 -pnpm lint:fix - -# 포맷팅 -pnpm format - -# 포맷팅 검사 -pnpm format:check - -# 타입 검사 -pnpm type-check -``` - -### 테스트 - -```bash -# 프론트엔드 테스트 -pnpm test - -# Rust 테스트 -cargo test - -# 모든 테스트 -pnpm test --run -cargo test --all-targets -``` - -### 빌드 - -```bash -# 개발 빌드 -pnpm tauri:dev - -# 프로덕션 빌드 -pnpm tauri:build -``` - -## CI/CD - -프로젝트는 GitHub Actions를 통해 자동화된 품질 관리를 제공합니다: - -- **JavaScript/TypeScript Lint**: oxlint를 통한 코드 품질 검사 -- **Rust Lint**: rustfmt와 clippy를 통한 Rust 코드 검사 -- **Frontend Test**: vitest를 통한 프론트엔드 테스트 -- **Rust Test**: cargo test를 통한 Rust 테스트 -- **Build Check**: 전체 프로젝트 빌드 검증 - -## 기여하기 - -1. 이슈를 생성하거나 기존 이슈를 확인하세요. -2. 포크를 생성하고 새 브랜치를 만드세요. -3. 변경사항을 구현하고 테스트하세요. -4. 커밋 메시지를 명확하게 작성하세요. -5. Pull Request를 생성하세요. - -### 커밋 컨벤션 - -``` -type(scope): description - -feat: 새로운 기능 추가 -fix: 버그 수정 -docs: 문서 수정 -style: 코드 포맷팅 -refactor: 코드 리팩토링 -test: 테스트 추가/수정 -chore: 빌드 설정 변경 -``` - -## 아키텍처 - -### FSD (Feature-Sliced Design) 아키텍처 - -ExecuteJS 프론트엔드는 FSD 아키텍처를 사용하여 코드를 체계적으로 구성합니다. - -#### 폴더 구조 - -``` -src/ -├── app/ # 앱 초기화 및 프로바이더 -│ ├── index.tsx # 앱 루트 컴포넌트 -│ └── providers.tsx # 전역 프로바이더 (Legend State, Radix UI) -├── pages/ # 페이지 레벨 컴포넌트 -│ └── playground/ -│ └── PlaygroundPage.tsx # 플레이그라운드 페이지 -├── widgets/ # 복합 UI 블록 -│ ├── code-editor/ -│ │ └── CodeEditor.tsx # Monaco Editor 래퍼 -│ ├── output-panel/ -│ │ └── OutputPanel.tsx # 출력 결과 패널 -│ └── tab-bar/ -│ └── TabBar.tsx # 탭 관리 UI -├── features/ # 비즈니스 로직 기능 -│ ├── execute-code/ -│ │ ├── model.ts # 실행 상태 및 로직 -│ │ └── api.ts # Tauri command 호출 -│ └── manage-tabs/ -│ └── model.ts # 탭 관리 상태 (localStorage 연동) -├── shared/ # 공유 유틸리티 -│ ├── ui/ # 공통 UI 컴포넌트 -│ ├── lib/ # 유틸 함수 -│ └── types/ # 공통 타입 -└── main.tsx # Vite 엔트리 -``` - -#### 레이어별 역할 - -- **app/**: 앱 초기화, 전역 프로바이더 설정 -- **pages/**: 페이지 레벨 컴포넌트 (라우팅 단위) -- **widgets/**: 복합 UI 블록 (여러 features 조합) -- **features/**: 비즈니스 로직 기능 (도메인별) -- **shared/**: 공유 유틸리티 (재사용 가능) - -#### 의존성 규칙 - -- 상위 레이어는 하위 레이어를 import 가능 -- 같은 레이어 내에서는 import 금지 -- shared는 다른 모든 레이어에서 import 가능 - -### 프론트엔드 기술 스택 - -- **상태 관리**: Zustand (경량 상태 관리) -- **UI 라이브러리**: Radix UI + Tailwind CSS -- **코드 에디터**: Monaco Editor -- **레이아웃**: react-resizable-panels -- **빌드 도구**: Vite -- **테스팅**: Vitest + Testing Library -- **린팅**: oxlint + Prettier - -### 백엔드 (Rust + Tauri) - -- **프레임워크**: Tauri 2.0 -- **JavaScript 엔진**: Deno Core 0.323 (V8 기반) -- **테스팅**: cargo test -- **린팅**: rustfmt -- **로깅**: tracing - -### 모노레포 구조 - -- **pnpm 워크스페이스**: Node.js 패키지 관리 -- **Cargo 워크스페이스**: Rust 크레이트 관리 -- **조건부 CI**: 폴더별 변경사항에 따른 워크플로우 실행 diff --git a/docs/docs/guide/getting-started.mdx b/docs/docs/guide/getting-started.mdx index 2d2f843..f5abc8d 100644 --- a/docs/docs/guide/getting-started.mdx +++ b/docs/docs/guide/getting-started.mdx @@ -58,7 +58,18 @@ executeJS/ └── ... ``` +## npm 모듈 사용하기 + +ExecuteJS는 npm 레지스트리에서 패키지를 직접 다운로드하고 사용할 수 있습니다. + +```javascript +import _ from 'npm:lodash'; +console.log(_.map([1, 2, 3], (x) => x * 2)); +``` + +자세한 내용은 [npm 모듈 사용하기](/guide/npm-modules) 가이드를 참조하세요. + ## 다음 단계 -- [API 참조](/api/commands)에서 사용 가능한 모든 명령어를 확인하세요. -- [개발 가이드](/guide/development)에서 프로젝트 개발 방법을 알아보세요. +- [npm 모듈 사용하기](/guide/npm-modules)에서 npm 패키지 사용 방법을 알아보세요 +- [개발하기](/dev/development)에서 프로젝트 개발 방법을 알아보세요 diff --git a/docs/docs/guide/npm-modules.mdx b/docs/docs/guide/npm-modules.mdx new file mode 100644 index 0000000..e02eb6f --- /dev/null +++ b/docs/docs/guide/npm-modules.mdx @@ -0,0 +1,103 @@ +# npm 모듈 사용하기 + +ExecuteJS는 npm 레지스트리에서 패키지를 직접 다운로드하고 사용할 수 있습니다. + +## 설치 + +ExecuteJS를 아직 설치하지 않으셨다면, [GitHub 릴리즈](https://github.com/ohah/executeJS/releases)에서 최신 버전을 다운로드하여 설치하세요. + +## 기본 사용법 + +### npm 패키지 import + +`npm:` 프로토콜을 사용하여 npm 패키지를 import할 수 있습니다. + +```javascript +import _ from 'npm:lodash'; + +console.log(_.map([1, 2, 3], (x) => x * 2)); +// 출력: [2, 4, 6] +``` + +### 버전 지정 + +특정 버전을 지정할 수도 있습니다. + +```javascript +import lodash from 'npm:lodash@4.17.21'; +``` + +### 스코프 패키지 + +`@scope/package` 형식의 스코프 패키지도 지원합니다. + +```javascript +import { something } from 'npm:@some-scope/package'; +``` + +## CommonJS, UMD 모듈 지원(지원 예정) + +ExecuteJS는 CommonJS, UMD 형식의 패키지를 자동으로 ES 모듈로 변환합니다. + +### 기본 import + +## 작동 방식 + +1. **패키지 다운로드**: npm 레지스트리 API를 통해 패키지를 다운로드합니다. +2. **로컬 캐시**: 다운로드한 패키지는 로컬 캐시에 저장되어 재사용됩니다. +3. **CommonJS 변환**: CommonJS 형식의 패키지는 자동으로 ES 모듈로 변환됩니다. +4. **모듈 로딩**: Deno Core를 통해 모듈을 로드하고 실행합니다. + +## 캐시 위치 + +다운로드한 패키지는 다음 위치에 캐시됩니다: + +- **macOS**: `~/Library/Caches/executejs/npm/` +- **Windows**: `%LOCALAPPDATA%/executejs/npm/` + +## 제한사항 + +### 지원되는 패키지 형식 + +현재는 **ESM 형식의 패키지**만 지원합니다. CommonJS, UMD 전용 패키지는 제한적으로 지원될 수 있습니다. + +## 예제 + +### Lodash 사용하기 + +```javascript +import _ from 'npm:lodash'; + +const data = [1, 2, 3, 4, 5]; +const result = _.map(data, (x) => x * 2); +console.log('Doubled:', result); +// 출력: Doubled: [2, 4, 6, 8, 10] +``` + +### 날짜 처리 (date-fns) + +```javascript +import { format, addDays } from 'npm:date-fns'; + +const now = new Date(); +console.log(format(now, 'yyyy-MM-dd HH:mm:ss')); + +// 날짜 계산 +const tomorrow = addDays(now, 1); +console.log(format(tomorrow, 'yyyy-MM-dd')); +``` + +## 문제 해결 + +### 패키지 로드 실패 + +패키지가 로드되지 않는 경우: + +1. **인터넷 연결 확인**: npm 레지스트리에 접근 가능한지 확인 +2. **패키지 이름 확인**: 패키지 이름이 정확한지 확인 +3. **버전 확인**: 지정한 버전이 존재하는지 확인 + +## 다음 단계 + +- [시작하기](/guide/getting-started)에서 기본 사용법을 확인하세요 +- [개발하기](/dev/development)에서 프로젝트 개발 방법을 알아보세요 diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index dc2b2ae..171dcb6 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -18,6 +18,8 @@ hero: - 🚀 **Tauri 기반**: Rust 백엔드와 React 프론트엔드 - 🔒 **안전한 실행**: 샌드박스 환경에서 JavaScript 실행 +- 📦 **npm 모듈 지원**: npm 레지스트리에서 패키지를 직접 사용 +- 🔄 **CommonJS 변환**: CommonJS 패키지를 자동으로 ES 모듈로 변환 - 📝 **실행 기록**: 코드 실행 이력 관리 - 💾 **코드 저장/로드**: 자주 사용하는 코드 저장 - 🎨 **모던 UI**: 직관적이고 반응형 사용자 인터페이스 @@ -29,4 +31,4 @@ hero: - **Backend**: Rust, Tauri 2.0 - **Testing**: Vitest, Testing Library - **Linting**: oxlint, Prettier -- **Documentation**: RSPress \ No newline at end of file +- **Documentation**: RSPress From 2cefee6d9f46e3c76334d5d9f65963ec44ac3ccf Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:24:06 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore(docs):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20API=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발하기 섹션으로 이동하여 파일 제거 --- docs/docs/api/commands.mdx | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 docs/docs/api/commands.mdx diff --git a/docs/docs/api/commands.mdx b/docs/docs/api/commands.mdx deleted file mode 100644 index 0e694b7..0000000 --- a/docs/docs/api/commands.mdx +++ /dev/null @@ -1,38 +0,0 @@ -# API 참조 - -ExecuteJS에서 사용할 수 있는 모든 Tauri 명령어를 확인하세요. - -## 기본 명령어 - -### execute_js - -자바스크립트에 실행을 요청하는 명령어입니다. - -```typescript -import { invoke } from '@tauri-apps/api/core'; - -const result = await invoke('execute_js', { - code: 'console.log("Hello, World!");', -}); -console.log(result); -``` - -**매개변수:** - -- `code` (string): 실행할 JavaScript 코드 - -**반환값:** - -- `JsExecutionResult`: 실행 결과 객체 - -## 에러 처리 - -모든 명령어는 Promise를 반환하며, 에러가 발생할 경우 적절한 에러 메시지와 함께 거부됩니다. - -```typescript -try { - const result = await invoke('execute_js', { code: 'invalid syntax' }); -} catch (error) { - console.error('Execution failed:', error); -} -``` From 89a72393a6fc10f814f111a670641309c3e3f6df Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 21:24:58 +0900 Subject: [PATCH 11/13] =?UTF-8?q?docs:=20=EA=B0=9C=EB=B0=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EC=97=90=20API=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발하기 섹션에서 API 참조 문서로 연결 --- docs/docs/dev/development.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/dev/development.mdx b/docs/docs/dev/development.mdx index 4d87957..674c226 100644 --- a/docs/docs/dev/development.mdx +++ b/docs/docs/dev/development.mdx @@ -206,3 +206,7 @@ crates/deno-runtime/src/ - **pnpm 워크스페이스**: Node.js 패키지 관리 - **Cargo 워크스페이스**: Rust 크레이트 관리 - **조건부 CI**: 폴더별 변경사항에 따른 워크플로우 실행 + +## 다음 단계 + +- [API 참조](/dev/api/commands)에서 사용 가능한 모든 Tauri 명령어를 확인하세요. From 4f43f4c91ddd4a5246db38e7ab0f91a7a3cc9068 Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 22:59:36 +0900 Subject: [PATCH 12/13] =?UTF-8?q?fix(docs):=20npm=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=EC=9D=98=20CommonJS=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=EC=84=A4=EB=AA=85=20=EB=AA=A8=EC=88=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CommonJS 변환 미구현 상태에 맞게 문서 수정 - 작동 방식에서 CommonJS 변환 단계 제거 - 지원되는 패키지 형식 섹션으로 통합 --- docs/docs/guide/npm-modules.mdx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/docs/guide/npm-modules.mdx b/docs/docs/guide/npm-modules.mdx index e02eb6f..70fad71 100644 --- a/docs/docs/guide/npm-modules.mdx +++ b/docs/docs/guide/npm-modules.mdx @@ -35,18 +35,17 @@ import lodash from 'npm:lodash@4.17.21'; import { something } from 'npm:@some-scope/package'; ``` -## CommonJS, UMD 모듈 지원(지원 예정) +## 지원되는 패키지 형식 -ExecuteJS는 CommonJS, UMD 형식의 패키지를 자동으로 ES 모듈로 변환합니다. +ExecuteJS는 **ES Module (ESM) 형식의 패키지**를 지원합니다. -### 기본 import +일부 CommonJS나 UMD 형식의 패키지도 사용할 수 있지만, 완벽하게 동작하지 않을 수 있습니다. ## 작동 방식 1. **패키지 다운로드**: npm 레지스트리 API를 통해 패키지를 다운로드합니다. 2. **로컬 캐시**: 다운로드한 패키지는 로컬 캐시에 저장되어 재사용됩니다. -3. **CommonJS 변환**: CommonJS 형식의 패키지는 자동으로 ES 모듈로 변환됩니다. -4. **모듈 로딩**: Deno Core를 통해 모듈을 로드하고 실행합니다. +3. **모듈 로딩**: Deno Core를 통해 모듈을 로드하고 실행합니다. ## 캐시 위치 @@ -55,12 +54,6 @@ ExecuteJS는 CommonJS, UMD 형식의 패키지를 자동으로 ES 모듈로 변 - **macOS**: `~/Library/Caches/executejs/npm/` - **Windows**: `%LOCALAPPDATA%/executejs/npm/` -## 제한사항 - -### 지원되는 패키지 형식 - -현재는 **ESM 형식의 패키지**만 지원합니다. CommonJS, UMD 전용 패키지는 제한적으로 지원될 수 있습니다. - ## 예제 ### Lodash 사용하기 From 84f8ffe459e188d64c9e9efd084172bd926dc8cb Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 2 Nov 2025 22:59:38 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix(deno-runtime):=20=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=ED=94=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=ED=8C=8C=EC=8B=B1=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EA=B3=84=EC=82=B0=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @scope/package@version 형식에서 second_at_pos 기준 오류 수정 package_spec 기준으로 올바른 인덱스 계산 --- crates/deno-runtime/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/deno-runtime/src/lib.rs b/crates/deno-runtime/src/lib.rs index 81805f0..e37918e 100644 --- a/crates/deno-runtime/src/lib.rs +++ b/crates/deno-runtime/src/lib.rs @@ -240,8 +240,9 @@ impl ModuleLoader for NpmModuleLoader { // @scope/package@version 형식 let after_first_at = &package_spec[1..]; if let Some(second_at_pos) = after_first_at.rfind('@') { - let scope_and_name = &package_spec[..=second_at_pos]; - let version = &package_spec[second_at_pos + 1..]; + // second_at_pos는 after_first_at 기준이므로 package_spec 기준으로는 +1 필요 + let scope_and_name = &package_spec[..=(second_at_pos + 1)]; + let version = &package_spec[second_at_pos + 2..]; (scope_and_name.to_string(), Some(version.to_string())) } else { // @scope/package (버전 없음)