개발도구·CLI
Vite+는 Vite, Rolldown, Vitest, Oxlint, Oxfmt, tsdown, Vite Task를 연결하고, vp라는 명령으로 웹 개발의 반복 작업을 수행하게 하는 도구입니다. 여기에 Node.js 버전과 패키지 관리자 관리까지 더합니다. 이 저장소의 핵심은 모든 도구를 새로 만드는 것이 아니라, 서로 다른 도구가 같은 환경에서 일하도록 연결하는 설계입니다.
웹 프로젝트를 시작하면 개발 서버, 포매터, 린터, 테스트 도구, 빌드 도구를 각각 골라야 합니다. 명령도 다르고 설정 파일도 늘어납니다. Vite+는 프로젝트의 vite.config.ts에 관련 설정을 모으고, 명령 이름과 도구 선택을 일관되게 만드는 방향을 택했습니다.
Vite는 개발 서버와 앱 빌드 작업대, Vitest는 검수 작업대, Oxlint와 Oxfmt는 코드의 규칙과 모양을 검사하는 작업대입니다. Vite+는 작업마다 알맞은 기계를 고르고, 같은 재료와 환경을 넘겨주는 관리자에 가깝습니다.
관리자가 생겨도 기계의 기능과 제약은 사라지지 않습니다. 그래서 이 코드를 읽을 때는 “어떤 알고리즘인가”와 함께 “누구에게 일을 맡기고 무엇을 전달하는가”를 물어야 합니다.
| 하고 싶은 일 | 명령 | 실제로 맡는 쪽 |
|---|---|---|
| 개발 서버 시작 | vp dev | 코어 패키지에 포함된 Vite의 실행 진입점 |
| 정적 검사 | vp check | Oxfmt → Oxlint, 설정에 따른 타입 검사 |
| 테스트 | vp test | CLI와 함께 배포되는 Vitest 우선 선택 |
| 앱 배포용 빌드 | vp build | Vite와 그 내부 번들러 Rolldown |
| 라이브러리 묶기 | vp pack | tsdown을 이용하는 패키징 경로 |
| 사용자 작업 실행 | vp run build | Vite Task가 선택한 스크립트 또는 작업 |
| 환경·의존성 관리 | vp env, vp install | Rust의 런타임 관리·패키지 관리자 계층 |
근거는 README.md, packages/cli/src/bin.ts, packages/cli/src/resolve-vite.ts, packages/cli/src/resolve-test.ts입니다. 명령에 따라 Rust만으로 처리하는 부분과 Node.js를 거치는 부분이 다릅니다.
vp는 입구이고, 입구 뒤에 여러 실행 엔진이 있습니다.# 내장 Vitest를 실행
vp test
# 프로젝트가 정의한 test 스크립트 또는 작업을 실행
vp run test
# vp run의 짧은 이름
vpr test
package.json에 "test": "jest"가 있다면 vp run test는 그 스크립트를 실행합니다. vp test는 여전히 내장 테스트 경로를 택합니다. 같은 이름의 스크립트로 내장 명령을 덮어쓰지 않는 규칙은 docs/guide/run.md와 packages/cli/src/bin.ts에 드러납니다.
명령 인자, 현재 디렉토리, 환경변수, 설정값, 종료 코드가 어디서 어디로 전달되는지 추적해 봅시다. 이 다섯 가지를 설명할 수 있으면 큰 도구 저장소도 작은 흐름으로 나눠 읽을 수 있습니다.
2026-10-23 GitHub 기준 스타는 6,458개, 포크는 296개입니다. GitHub Trending Rust Weekly #10이라는 순위는 발견의 계기입니다. 이 순위만으로 특정 기능이 인기를 끌었다거나 사용자가 급증했다고 증명할 수는 없습니다. 여기서는 소스에서 드러나는 매력을 세 가지로 해석합니다.
packages/cli/src/define-config.ts가 Vite의 설정 타입에 lint, fmt, run, staged 등을 추가합니다.Vite+의 장점은 명령 몇 글자를 줄이는 데서 끝나지 않고, 팀이 합의해야 할 도구 조합과 실행 규칙을 함께 줄이는 데 있습니다.
다음은 역할과 구성 방식을 비교한 표입니다. 실행 속도나 호환성 전체를 측정한 결과가 아니며, 다른 도구도 추가 설정과 플러그인으로 범위를 넓힐 수 있습니다.
| 선택지 | 주로 맡기는 일 | Vite+가 더 묶는 부분 | 고려할 비용 |
|---|---|---|---|
| Vite 단독 | 개발 서버와 앱 빌드 | 검사·테스트·작업 실행·환경 관리의 공통 입구 | Vite+가 묶어 배포하는 버전 관계도 이해해야 함 |
| pnpm 중심 구성 | 의존성 설치와 스크립트 실행 | 도구 선택·통합 설정·작업 캐시를 덧붙임 | vp test와 vp run test를 구분해야 함 |
| 도구 직접 조합 | Vite·검사기·테스트 도구를 개별 선택 | 함께 동작할 조합과 설정 타입을 제공 | 개별 도구 버전을 자유롭게 바꾸던 방식의 재검토 |
| Vite+ | 개발 흐름 전체를 하나의 CLI로 연결 | 프로젝트별 CLI와 머신의 환경 관리까지 연결 | 기존 설정·의존성·플러그인에 대한 마이그레이션 확인 |
Vite와 pnpm은 이 프로젝트가 없애려는 대상으로만 보면 안 됩니다. Vite는 코어에 포함되고 pnpm은 관리 가능한 패키지 관리자 중 하나입니다. 이미 검증된 도구를 조합하면서 반복되는 접착 코드를 제품으로 만든다는 관점이 더 정확합니다.
도구 직접 조합은 부품을 골라 조립하는 데스크톱 PC와 비슷합니다. Vite+는 함께 동작할 부품 조합을 정한 완제품에 가깝습니다. 처음 맞추기는 편해지지만, 특정 부품만 교체할 때는 전체 조합을 확인해야 합니다.
README.md는 기본 설정만으로 시작하는 도구 모음이라고 소개합니다. 실제로는 vite.config.ts에서 세밀한 설정을 받으며, vp check의 타입 검사도 조건부입니다. 이 저장소의 vite.config.ts는 lint.options.typeAware와 typeCheck를 모두 켭니다. “아무 설정도 필요 없다”보다 “기본값을 주되 한곳에서 조정한다”로 이해하면 맞습니다.
packages/cli/src/resolve-test.ts는 Vitest를 CLI 쪽에서 먼저 찾습니다. 테스트 실행기와 vite-plus/test에서 불러오는 API가 서로 다른 복사본을 가리켜 내부 상태가 갈라지는 일을 줄이려는 선택입니다. 도구 통합에서 버전 정합성이 왜 중요한지 보여줍니다.
Rust를 썼다는 사실만으로 모든 명령이 빠르다고 결론 낼 수 없습니다. Node.js 시작, 설정 평가, 파일 수, 캐시 적중 여부, 기존 플러그인까지 영향을 줍니다. 이 글은 독립적인 속도 측정치를 제시하지 않으며, 팀 프로젝트에서는 같은 입력과 캐시 상태로 비교해야 합니다.
이 제품은 데이터베이스를 둔 웹 서비스가 아니라 개발자 컴퓨터와 CI에서 실행하는 도구입니다. 따라서 백엔드는 서버 API보다 실행 코어에 해당합니다. 프론트엔드도 제품의 필수 웹 화면과 문서 사이트를 구분해서 봐야 합니다.
Cargo.toml이나 package.json은 허용할 버전을 선언하고, 잠금 파일은 해결된 버전을 기록합니다. Cargo의 "4.5.40"은 정확히 그 버전만 고정한다는 뜻이 아닙니다. catalog:는 실제 번호를 pnpm-workspace.yaml의 공통 목록에서 찾으라는 뜻입니다.| 기술 | 선언 → 잠금 값 | 이 저장소에서의 역할 |
|---|---|---|
| Rust | edition 2024, rust-version = "1.92.0" | 워크스페이스의 문법 기준과 최소 버전 선언 |
| 개발 도구 모음 | nightly-2026-08-02 | rust-toolchain.toml의 실제 개발용 선택, rust-src 포함 |
| Clap | 4.5.40 → 4.6.7 | 명령·옵션 파싱과 도움말 체계 |
| Tokio | 1.48.0 → 1.54.1 | 비동기 파일·프로세스·다운로드 작업의 실행 기반 |
| Serde | 1.0.219 → 1.0.229 | 설정과 도구 사이 데이터 직렬화 |
| NAPI-RS | napi 3.12.2 → 3.14.2 | Node.js와 Rust 네이티브 코드 사이 연결 |
| Oxc Parser | 0.155.0 → 0.155.0 | 설정 파일을 AST로 읽고 정적 값 추출 |
| Oxc Resolver | 11.24.3 → 11.24.3 | 프로젝트의 패키지 경로 찾기 |
| Vite Task | Git revision a3c087a7… | vt, vt_workspace 등 외부 Rust 의존성 |
선언은 루트 Cargo.toml, 잠금 값은 Cargo.lock 기준입니다. crates/vp_global_cli/Cargo.toml은 vp 실행 파일을 정의하고, packages/cli/binding/Cargo.toml은 crate-type = ["cdylib"]로 Node.js가 불러올 네이티브 라이브러리를 만듭니다.
Rust 최소 버전 선언이 있어도 이 저장소 전체를 stable Rust만으로 빌드할 수 있다고 읽으면 안 됩니다. rust-toolchain.toml은 fspy의 사전 로드 라이브러리 빌드에 필요한 -Z bindeps를 nightly 선택 이유로 설명합니다. Windows용 vp_trampoline도 별도 설정으로 표준 라이브러리를 빌드합니다.
| 항목 | 확인한 버전 | 근거와 역할 |
|---|---|---|
| Vite+ CLI / 코어 | 1.4.0 / 1.4.0 | packages/cli/package.json, packages/core/package.json |
| Vite | 8.3.5 | 코어의 bundledVersions.vite에 기록 |
| Rolldown | 1.2.15 | 코어의 bundledVersions.rolldown에 기록 |
| tsdown | 0.23.1 | 코어의 bundledVersions.tsdown에 기록 |
| Vitest | 5.0.5 | 공통 catalog의 고정값, CLI의 의존성 |
| Oxlint / Oxfmt | =1.89.0 / =0.74.0 | 공통 catalog에서 정확한 버전을 요구 |
| oxlint-tsgolint | =7.0.2004 | 타입 정보를 이용하는 검사 경로 |
| TypeScript | ~7.0.2 | 개발 의존성의 허용 범위, 번들 버전과 구분 |
| pnpm | 12.11.2 | 루트 packageManager 값 |
모든 도구가 같은 방식으로 들어오는 것은 아닙니다. packages/cli/toolchain.config.json은 Vite와 tsdown을 bundled, Vitest·Oxlint·Oxfmt를 dependency, Vite Task를 compiled로 구분합니다. README의 “단일 의존성”은 사용자의 진입점이 단순하다는 뜻이며, 내부 npm 의존성이 하나라는 뜻은 아닙니다.
// packages/cli/package.json의 핵심 부분
{
"version": "1.4.0",
"dependencies": {
"vite": "workspace:@voidzero-dev/vite-plus-core@*",
"vitest": "catalog:",
"oxlint": "catalog:",
"oxfmt": "catalog:"
}
}
vite라는 의존성 이름이 내부 코어 패키지를 가리킵니다. packages/cli/BUNDLING.md는 배포 시 이 관계를 정확한 npm 별칭으로 바꾸고, CLI와 플러그인이 같은 코어를 보도록 유지한다고 설명합니다. 이름을 단순히 바꾸는 것보다 같은 모듈을 공유하도록 보장하는 일이 중요합니다.
주 사용 인터페이스는 터미널입니다. 문서 사이트는 docs/의 VitePress와 Vue로 구성되며, 웹 앱을 만드는 사용자는 React·Vue 등 자신의 프레임워크를 따로 선택합니다. 문서가 Vue로 작성됐다고 Vite+가 Vue 전용 빌드 도구가 되는 것은 아닙니다.
| 문서 사이트 기술 | docs/package.json 선언 | 용도 |
|---|---|---|
| VitePress | 2.0.0-alpha.20 | 문서 생성과 개발 서버 |
| Vue | ^3.5.27 | 문서 테마와 화면 구성 요소 |
| Tailwind CSS | ^4.1.18 | 문서 화면 스타일링 도구 |
| Mermaid / Reka UI | ^12.0.0 / ^2.7.0 | 도식과 문서 화면 상호작용 |
docs/는 별도의 pnpm-lock.yaml을 둡니다. 루트 catalog에 적힌 Vue ^3.5.35와 VitePress ^2.0.0-alpha.15를 문서 패키지의 직접 선언과 혼동하지 맙시다. 같은 이름의 의존성도 어느 패키지의 선언인지 확인해야 합니다.
3, JavaScript는 pnpm workspace로 여러 패키지를 관리합니다. vp_trampoline은 Cargo 워크스페이스에서 제외된 별도 패키지입니다.packages/tools/.upstream-versions.json에 Vite·Rolldown·oxc-node의 커밋을 적고, .github/actions/clone/action.yml이 각각 받아옵니다..github/workflows/ci.yml의 CLI E2E 행렬은 Linux x64 + Node.js 22.18.0, macOS arm64 + Node.js 24.11.0, Windows x64 + Node.js 26.0.0의 세 조합을 명시합니다..github/actions/setup-ci-node/action.yml의 비 Linux 경로는 pnpm install --frozen-lockfile --ignore-scripts를 사용합니다.docker/Dockerfile은 debian:bookworm-slim을 바탕으로 Git·C/C++ 빌드 도구·Python 등을 넣는 개발·CI용 이미지입니다.이 저장소는 Python 서버나 Go API를 중심으로 돌아가지 않습니다. Docker에 Python이 있다는 사실도 Python 제품이라는 뜻이 아닙니다. 네이티브 npm 의존성을 빌드할 도구와 제품의 주 실행 언어를 구분해서 읽는 연습을 할 수 있습니다.
아래 구조도는 vp dev처럼 JavaScript 도구에 위임하는 대표 경로입니다. 모든 명령이 이 전체 경로를 통과하지는 않습니다. 환경·패키지 관리자 명령 중에는 글로벌 Rust CLI가 직접 처리하는 경로도 있습니다.
NAPI 호출은 새 서버에 보내는 요청이 아닙니다. Node.js 프로세스 안에 불러온 Rust 네이티브 모듈과의 호출입니다. 반대로 도구를 자식 프로세스로 실행하는 부분은 운영체제의 프로세스 경계를 넘습니다. 두 종류의 경계를 구별해야 흐름이 보입니다.
crates/vp_global_cli/src/js_executor.rs의 JsExecutor::resolve_local_vite_plus_package_dir()는 Oxc Resolver로 vite-plus/package.json을 찾습니다. 이어 resolve_local_vite_plus()가 dist/bin.js의 존재를 검사합니다. prepare_js_entry()는 그 경로가 있으면 선택하고, 없으면 글로벌 설치의 bin.js를 택합니다.
이 구조 덕분에 한 머신의 글로벌 입구를 유지하면서 프로젝트마다 다른 CLI 버전을 사용할 수 있습니다. 단, 모든 명령이 무조건 로컬 우선인 것은 아닙니다. 같은 파일의 delegate_migrate()는 로컬 버전이 글로벌보다 오래된 업그레이드 상황에서 글로벌 마이그레이션으로 올려 보내는 별도 정책을 가집니다.
// packages/cli/src/bin.ts: run()에 전달하는 항목 일부
const exitCode = await run({
nodeExecPath: process.execPath,
lint,
pack,
fmt,
vite,
test,
doc,
// 이 밖에 설정 resolver, 인자, 패키지 경로 등을 전달
});
위 코드는 핵심 항목만 발췌한 것입니다. packages/cli/binding/src/lib.rs의 CliOptions는 이 함수들을 ThreadsafeFunction으로 받습니다. create_resolver()는 JS 콜백이 반환한 Promise를 기다려 bin_path와 envs를 Rust 실행 계층의 결과로 바꿉니다.
초보자가 볼 지점은 Rust가 npm 패키지의 모든 배치 규칙을 혼자 구현하지 않는다는 것입니다. TypeScript 쪽의 packages/cli/src/resolve-test.ts나 packages/cli/src/resolve-vite.ts가 경로를 알아내고, Rust는 정해진 결과 형태를 이용합니다. 언어를 나눌 때 책임도 함께 나누는 사례입니다.
crates/vp_static_config/src/lib.rs의 resolve_static_config()는 Oxc Parser로 설정을 읽습니다. 설정 파일 자체를 실행하지 않고 AST에서 값을 추출합니다. packages/cli/binding/src/cli/handler.rs의 VitePlusConfigLoader::load_user_config_file()은 그 결과의 run 필드에 따라 세 갈래로 움직입니다.
| 조회 결과 | 뜻 | 호출자의 선택 |
|---|---|---|
Json(value) | 실행 없이 읽을 수 있는 값 | 바로 UserRunConfig로 역직렬화 |
NonStatic | 존재하지만 동적이거나, 있을 수 있어 확정 불가 | NAPI를 통해 JS 설정 로딩으로 대체 |
None | 해당 필드가 없다고 확정 | 이 경로에서 JS 설정 평가 없이 반환 |
// 학습용 비교: 첫 번째 run은 정적으로 읽을 수 있음
export default {
run: { tasks: { hello: { command: 'node hello.js' } } },
};
// 변수의 실제 값까지 따라 계산하지 않으므로 대체 경로 필요
const tasks = makeTasks();
export default { run: { tasks } };
이 예시는 서로 다른 두 설정을 비교한 것이므로 한 파일에 함께 넣지는 않습니다. 실제 구현의 FieldMap은 내부에 open이라는 상태를 둡니다. { run: oldRun, ...extra }에서는 뒤의 전개가 run을 바꿀 수 있습니다. extract_object_fields()는 전개를 만나면 앞서 모은 필드를 지우고, 뒤에서 명시한 값만 확실한 정보로 남깁니다.
정적 분석기의 중요한 능력은 모든 값을 계산하는 것이 아니라, 확실히 아는 값과 아직 모르는 값을 구분하는 것입니다.
crates/vp_static_config/README.md는 반환형을 Option<FxHashMap<…>>으로 설명합니다. 커밋 d3d9f57의 실제 함수는 FieldMap을 반환합니다. 설정 파일 없음은 닫힌 빈 맵, 분석 불가는 열린 빈 맵입니다. README의 설명만으로 “파일 없음”과 “해석 불가”를 같은 상태로 구현하면 현재 동작을 놓칩니다.
대체 경로인 packages/cli/src/resolve-vite-config.ts의 resolveUniversalViteConfig()는 설정을 로딩하고 lint, fmt, check, run, staged 등을 JSON으로 돌려줍니다. 정적 추출이 있다고 해서 모든 명령에서 JavaScript 실행이 없어지는 것은 아닙니다.
packages/cli/binding/src/check/mod.rs의 execute_check()는 검사기를 단순히 동시에 켜지 않습니다. 설정과 옵션을 읽고, 형식 검사·수정 뒤 린트 단계를 실행하며, 수정 모드에서는 마지막 포맷을 다시 적용할 수 있습니다.
왜 마지막 포맷이 필요할까요? 린터의 자동 수정이 중괄호를 추가하면 처음 포맷했을 때와 코드 모양이 달라질 수 있기 때문입니다. “수정 단계들의 순서를 어떻게 정해야 최종 결과가 안정적인가”라는 작은 작업 흐름 설계입니다.
// 학습용 최소 설정: 두 옵션을 함께 켬
import { defineConfig } from 'vite-plus';
export default defineConfig({
lint: {
options: {
typeAware: true,
typeCheck: true,
},
},
});
packages/cli/binding/src/check/analysis.rs의 lint_config_type_check_enabled()는 두 옵션의 논리곱을 사용합니다. --no-lint는 린트 규칙 단계를 끄더라도 활성화된 타입 검사는 남길 수 있습니다. --no-fmt --no-lint를 주고 타입 검사도 꺼져 있으면 execute_check()는 검사할 것이 없다는 메시지를 출력하고 종료 상태 1을 반환합니다.
다른 학습 포인트도 있습니다. 결과 분석기가 도구의 요약 출력을 읽으므로, 이 함수는 린트 호출에 --format=default를 넣습니다. GitHub Actions 환경에서 출력 형식이 달라져 파서가 오판하는 일을 막기 위해서입니다. 외부 도구의 출력도 통합 계층에서는 하나의 계약이 됩니다.
분석 기준 커밋의 주제는 Git 설정 잠금입니다. 연결된 여러 worktree가 같은 Git 설정을 쓰면, 동시에 실행한 vp config가 config.lock을 두고 충돌할 수 있습니다. 핵심 구현은 Rust가 아닌 packages/cli/src/config/hooks.ts의 writeGitConfig()에 있습니다.
// 실제 상수와 제어 흐름을 줄인 의사 코드
const delays = [50, 100, 200, 400, 800];
let result = writeOnce();
for (const delay of delays) {
if (!isLockFailure(result.status)) break;
wait(delay);
if (isApplied()) return success;
result = writeOnce();
}
return result;
실제 상수 GIT_CONFIG_LOCK_RETRY_DELAYS_MS는 [50, 100, 200, 400, 800]입니다. 잠금 실패 상태 255와 0xffffffff일 때만 기다립니다. 최초 시도에 더해 최대 다섯 번 재시도하고, 명시한 대기 합계는 쓰기 한 번당 최대 1.55초입니다. Git 실행 시간까지 포함한 전체 명령 시간의 상한은 아닙니다.
gitConfigSet()은 값이 이미 같으면 쓰지 않습니다. 기다린 뒤에도 isApplied()로 다시 읽어, 다른 프로세스가 원하는 값을 저장했다면 성공으로 끝냅니다. Git 자체 대기와 중복되지 않도록 core.configLockTimeout=0도 명령 인자로 지정합니다.
공용 게시판에 같은 공지를 붙이려는 두 사람을 생각해 봅시다. 한 사람이 자리를 쓰고 있으면 잠깐 기다리고, 다시 봤을 때 공지가 이미 붙어 있다면 더 붙일 필요가 없습니다. “기다림”과 “이미 완료됐는지 확인”을 함께 넣는 것이 핵심입니다.
이를 멱등성과 지수형 재시도의 조합으로 읽을 수 있습니다. 다만 sleepSync()는 Atomics.wait()로 현재 스레드를 멈춥니다. 짧게 끝나는 설정 CLI에서 택한 방식이며, 장시간 동작하는 웹 서버의 요청 처리에 그대로 옮기는 방식은 별도 검토가 필요합니다.
packages/cli/src/config/__tests__/hooks.spec.ts에는 값이 같을 때 잠금을 새로 잡지 않는 경우, 잠금이 풀리기를 기다리는 경우, 다른 실행이 같은 값을 쓴 경우를 다루는 테스트가 있습니다. 기능 설명과 테스트 시나리오를 나란히 읽으면 재시도의 목적이 더 선명해집니다.
아래는 주요 경로를 추린 지도입니다. 파일 경로는 모두 저장소 루트 기준입니다. 디렉토리 이름만 외우기보다, “이 폴더가 어느 실행 경계를 책임지는가”를 함께 적어 보세요.
| 궁금한 질문 | 첫 번째로 열 파일 | 이어 읽을 곳 |
|---|---|---|
| 명령을 누가 받나? | crates/vp_global_cli/src/cli.rs | packages/cli/src/bin.ts |
| 도구 경로는 누가 찾나? | packages/cli/src/resolve-vite.ts | packages/cli/src/resolve-core.ts, packages/cli/src/resolve-test.ts |
| Rust 연결부는? | packages/cli/binding/src/lib.rs | packages/cli/binding/src/cli/mod.rs |
| 타입 검사 조건은? | packages/cli/binding/src/check/analysis.rs | packages/cli/binding/src/check/mod.rs, docs/guide/check.md |
| 작업 캐시 설정은? | docs/config/run.md | packages/cli/binding/src/cli/handler.rs, 외부 Vite Task |
| 도구를 어떻게 묶나? | packages/core/build.ts | packages/cli/build.ts, 각 패키지의 빌드 설명 문서 |
Vite와 Rolldown의 본체 소스가 이 저장소에 모두 직접 들어 있는 구조는 아닙니다. .github/actions/clone/action.yml은 별도 저장소의 고정 커밋을 각각 vite/, rolldown/, oxc-node/로 가져옵니다. Vite Task 크레이트도 Cargo.toml의 Git 의존성입니다. 이 경계를 알아야 통합 코드의 줄 수를 엔진 전체 규모로 오해하지 않습니다.
packages/core/build.ts에는 buildVite(), bundleRolldown(), bundleTsdown(), mergePackageJson()이 있습니다. packages/core/build-support/rewrite-imports.ts의 RewriteImportsPlugin은 import 경로를 코어 패키지 쪽으로 바꿉니다. 빌드 스크립트도 제품의 모듈 구성을 결정하는 소스입니다.
커밋 d3d9f57에서 Git이 추적하는 4,949개 경로를 대상으로 확장자별 물리적 줄 수를 합산했습니다. 빈 줄·주석·테스트·예제·생성 파일도 포함합니다. TypeScript는 .ts/.tsx/.mts/.cts, JavaScript는 .js/.jsx/.mjs/.cjs를 합쳤습니다.
| 언어·형식 | 파일 수 | 줄 수 | 해석할 때 주의할 점 |
|---|---|---|---|
| Rust | 255 | 103,698 | 실행 코드와 Rust 테스트를 함께 포함 |
| TypeScript | 730 | 94,836 | CLI·빌드·테스트·타입 선언·픽스처 포함 |
| JavaScript | 355 | 9,664 | 스크립트와 테스트 입력 파일 등 포함 |
| Vue | 23 | 2,065 | 주로 문서 사이트의 화면 구성 |
| Shell / PowerShell | 15 / 9 | 3,242 / 2,420 | 설치·검증·플랫폼별 보조 스크립트 |
| YAML | 148 | 31,016 | CI와 잠금 파일을 포함한 설정 형식 |
이 표는 선택한 확장자의 집계이며 모든 형식을 합친 전체 줄 수 표는 아닙니다. 숫자는 코드 복잡도나 품질의 점수가 아닙니다. 특히 YAML에는 큰 잠금 파일이 들어가고, 테스트 입력도 파일 수를 늘립니다. 별도 의존성인 Vite·Rolldown·Vite Task의 전체 소스 규모는 포함하지 않습니다.
처음부터 전체 저장소를 읽으려 하지 않아도 됩니다. 한 기술마다 “읽을 파일 하나, 설명할 동작 하나, 바꿔 생각할 입력 하나”를 정하면 학습 범위가 구체적이 됩니다.
| 기술 | 배울 내용과 근거 | 작은 실습 아이디어 |
|---|---|---|
| Rust의 enum | FieldValue로 정상 값과 정적 분석 불가를 표현 | Json, NonStatic, 필드 부재의 예를 각각 작성 |
| Rust의 Result | execute_check()가 명령 실행 오류와 검사 종료 상태를 나누어 반환 | 실행 파일 없음과 코드 검사 실패를 다른 칸에 분류 |
| 비동기 연결 | create_resolver()가 JS 호출과 반환 Promise를 기다림 | 콜백 호출 성공·Promise 거절을 별도 오류 경로로 그리기 |
| TypeScript 타입 확장 | packages/cli/src/define-config.ts의 declare module 'vite' | 설정 키가 편집기에 어떻게 보일지 타입 정의로 추적 |
| 파서와 AST | parse_js_ts_config(), extract_object_fields() | 전개 위치를 바꾸고 확정 가능한 키를 손으로 계산 |
| Node.js 모듈 탐색 | packages/cli/src/resolve-test.ts와 packages/cli/src/utils/constants.ts | 전역 CLI 선택과 Vitest 복사본 선택을 다른 단계로 구분 |
| 운영체제 프로세스 | crates/vp_global_cli/src/js_executor.rs의 명령·인자·현재 디렉토리 전달 | 공백이 있는 경로가 왜 단순 문자열 조합에 취약한지 설명 |
| 동시성 | packages/cli/src/config/hooks.ts의 재시도와 완료 여부 재조회 | 두 실행이 같은 설정을 쓰는 시간표 작성 |
| CI와 테스트 | .github/workflows/ci.yml, vp_cli_snapshots | 로직 검증과 터미널 출력 검증의 차이를 표로 정리 |
문법을 이해했다는 기준은 코드를 그대로 따라 쓰는 것이 아니라, 입력이 달라졌을 때 어느 분기로 가는지 설명할 수 있는가입니다.
execute_check()의 반환형은 Result<ExitStatus, Error>입니다. 바깥 Result는 실행 과정의 오류를 표현하고, 안쪽 종료 상태는 실행한 검사가 성공했는지를 표현합니다. 코드에 린트 문제가 있는 것과 프로세스를 시작하지 못한 것은 사용자에게 다른 안내가 필요합니다.
// 설명용 모델: 실제 함수를 복제한 코드는 아님
enum CheckOutcome {
Passed,
DiagnosticsFound,
CouldNotStart,
}
// 세 결과를 모두 단순한 false로 바꾸면 복구 방법도 사라짐
packages/cli/src/define-config.ts는 Vite의 UserConfig에 필드를 추가합니다. 이를 모듈 보강이라고 부릅니다. check.fmt와 check.lint의 주석을 읽으면, 이 설정은 통합 검사에만 적용되고 개별 vp fmt와 vp lint에는 적용되지 않는다는 계약도 알 수 있습니다.
타입이 있다고 값이 실행 시점에도 자동 검증되는 것은 아닙니다. Rust 쪽에서는 JSON을 UserRunConfig로 역직렬화하는 단계가 따로 있습니다. 편집기의 안내, 데이터 전달, 실행 시점 검증을 세 층으로 나누어 보세요.
docs/config/run.md의 기본값은 run.cache = { scripts: false, tasks: true }입니다. 즉 package.json 스크립트는 기본적으로 캐시하지 않고, vite.config.ts에 정의한 작업은 기본적으로 캐시합니다. 모든 vp run이 자동으로 같은 캐시 정책을 가지지는 않습니다.
// 학습용 작업 설정
import { defineConfig } from 'vite-plus';
export default defineConfig({
run: {
tasks: {
build: {
command: 'vp build',
cache: { env: ['NODE_ENV'] },
},
},
},
});
NODE_ENV가 결과를 바꾸는 입력이라면 캐시 판단에도 영향을 주어야 합니다. 이 예시는 설정 계약을 배우기 위한 것입니다. 파일·환경 자동 추적과 스케줄러의 내부 구현은 별도 Vite Task 저장소를 더 읽어야 설명할 수 있습니다.
“기본값은 scripts: false다”는 문서의 사실입니다. “부작용 있는 스크립트를 무심코 캐시하지 않도록 돕는다”는 설계 해석입니다. “두 번째 실행이 몇 초 빨랐다”는 측정 결과입니다. 이 세 문장을 섞지 않으면 기술 설명의 신뢰도가 올라갑니다.
Vite+로 작은 웹 앱을 개발하는 것과 Vite+ 자체의 Rust·네이티브 모듈·상류 도구까지 빌드하는 것은 부담이 다릅니다. 요구사항을 판단할 때는 완성된 도구를 쓰는 경우와 그 도구를 만드는 경우를 먼저 나누어야 합니다.
| 구분 | 확인된 조건 | 근거 |
|---|---|---|
| 운영체제·CPU | Linux·macOS·Windows, x64·arm64 계열 | crates/vp_js_runtime/src/platform.rs의 Os, Arch |
| 로컬 CLI의 Node.js | ^22.18.0 || ^24.11.0 || >=26.0.0 | packages/cli/package.json의 engines.node |
| 코어 패키지의 Node.js | ^20.19.0 || ^22.18.0 || >=24.11.0 | packages/core/package.json, CLI 조건과 구분 |
| 저장소 개발용 Node.js | 22.23.4 | 루트 .node-version |
| 저장소 개발용 pnpm | 12.11.2 | 루트 package.json |
| 소스 빌드 도구 | Rust/Cargo, Just, CMake, cargo-binstall 등 | CONTRIBUTING.md, justfile |
| 특수 가속기 | 핵심 CLI 경로에 전용 GPU 요구 선언 없음 | 실행 코어·패키지 의존성의 성격 기준 |
| RAM·디스크 최소값 | 고정된 공식 최솟값은 위 설정·기여 가이드에 명시되지 않음 | 프로젝트 크기·빌드 범위에 따라 달라짐 |
글로벌 vp는 사전 설치된 Node.js 없이 시작할 수 있는 Rust 실행 파일입니다. 그러나 Vite·Vitest 같은 JavaScript 도구를 쓸 때는 실제 Node.js가 필요하며, 관리 기능이 적절한 런타임을 준비합니다. “글로벌 바이너리가 독립적”이라는 말과 “Node.js를 전혀 쓰지 않음”은 다릅니다.
packages/cli/package.json의 napi.targets는 8개 대상을 명시합니다. macOS x64·arm64, Windows x64·arm64, Linux x64·arm64의 GNU·musl 조합입니다. docs/guide/index.md는 Linux x64 musl을 실험적 대상으로 분류하며, Alpine에서는 관리형 Node.js 실행을 위해 libstdc++가 필요하다고 설명합니다.
운영체제 이름이 같아도 C 표준 라이브러리가 다르면 같은 네이티브 파일을 사용할 수 없습니다. 특히 Linux의 GNU와 musl 대상이 나뉘는 이유를 이해하면, npm의 선택적 네이티브 패키지가 왜 필요한지도 배울 수 있습니다.
x86_64-unknown-linux-gnu 같은 문자열은 CPU·운영체제·실행 환경을 묶어 표시합니다. 네이티브 모듈은 기계어로 빌드되므로 이 조합을 맞춰야 합니다. JavaScript 파일처럼 모든 환경에 똑같이 복사해서 실행하는 파일과 다릅니다.| 학습 범위 | 계획용 여유 예시 | 먼저 확인할 것 |
|---|---|---|
| 소스·문서 읽기 | 기존 개발용 컴퓨터와 편집기로 시작 | 추가 엔진 빌드나 브라우저 설치는 필요 없음 |
| 작은 예제 앱 | RAM 8GB 안팎, 의존성을 둘 수 GB의 여유 | 앱 크기·브라우저·병렬 실행 수에 따라 조정 |
| 도구 자체 전체 빌드 | RAM 16~32GB, 디스크 수십 GB 여유를 계획 | Cargo 산출물·상류 소스·npm 캐시·브라우저까지 증가 |
위 RAM·저장공간 숫자는 공식 최소 사양이나 실측 결과가 아닌 학습 계획용 추정입니다. 새 장비를 고르는 기준으로 단정하지 말고, 작은 범위에서 시작해 실제 사용량을 보며 확장하세요. 병목은 GPU보다 메모리·디스크·병렬 컴파일에서 생길 가능성을 먼저 살펴보는 편이 합리적입니다.
docker/Dockerfile은 개발·CI·빌드 단계용으로 여러 도구를 포함합니다. 기본 사용자는 vp이고, 추가 개발 패키지를 설치할 수 있도록 암호 없는 sudo 설정도 둡니다. docs/guide/docker.md는 제품 배포에서는 다단계 빌드로 결과물과 필요한 Node.js만 작은 실행 이미지에 옮기는 방식을 안내합니다.
처음 공부할 때 전체 네이티브 빌드를 목표로 삼을 필요는 없습니다. 설정과 명령 흐름을 종이에 추적하는 것만으로도 어댑터, 정적 분석, 종료 코드, 재시도라는 핵심 주제를 익힐 수 있습니다.
과제 1·2·4는 읽기와 손으로 추적하기만으로 진행할 수 있습니다. 과제 3의 실행은 Vite+가 준비된 별도 예제 프로젝트에서 선택적으로 진행합니다. 과제 5도 우선 설계와 예측 결과를 작성하고, 구현은 독립된 연습 파일에서 확장하면 됩니다.
각 실습은 실행했다는 기록보다, 예상한 결과와 그 근거가 되는 코드 위치를 함께 남기는 것을 목표로 합니다.
목표: 내장 명령과 사용자 스크립트의 차이를 이해합니다. 아래 가상 프로젝트를 보고 각 명령이 무엇을 실행할지 적으세요.
{
"scripts": {
"dev": "custom-dev-server",
"test": "jest",
"build": "node compile.js"
}
}
vp dev, vp run dev, vp test, vpr test, vp run build의 실행 대상을 적습니다.docs/guide/run.md와 packages/cli/src/bin.ts에서 각각 근거 한 군데를 찾습니다.완료 기준: vp test는 Vitest, vpr test는 Jest 스크립트라고 구분하고, 스크립트가 내장 명령을 덮어쓰지 않는 이유를 설명합니다. 가상 명령은 실제 설치 없이 분류하면 됩니다.
목표: 정적 분석이 무엇을 확정할 수 있는지 예측합니다. 아래 세 줄은 서로 다른 설정 파일의 예시입니다.
// A
export default { plugins: [] };
// B
export default { run: { tasks: {} } };
// C
export default { run: { tasks: {} }, ...extra };
FieldMap::get("run")을 None, Json, NonStatic 중에서 고릅니다.crates/vp_static_config/src/lib.rs의 extract_object_fields()에서 대응하는 분기를 표시합니다.완료 기준: A는 None, B는 Json, C는 NonStatic으로 설명합니다. C에서 전개 뒤에 run을 명시하면 그 값은 확정할 수 있다는 점도 연결합니다.
목표: 포맷 문제, 린트 문제, 타입 문제, 실행 시작 실패를 구분합니다. Vite+와 TypeScript가 준비된 별도 예제에 4절의 typeAware/typeCheck 설정을 적용합니다.
// 타입 오류를 포함하는 학습용 입력
export const total: number = '세 개';
// 비교할 명령
vp check
vp check --no-fmt
vp check --no-fmt --no-lint
3으로 바꿨을 때와 바꾸기 전을 비교합니다.execute_check()와 lint_config_type_check_enabled()로 설명을 검산합니다.완료 기준: 두 옵션이 켜져 있으면 --no-lint여도 타입 검사가 남을 수 있고, 둘 다 꺼진 채 검사 단계까지 모두 끄면 실행할 검사가 없다는 메시지와 함께 종료 코드 1로 끝난다고 설명합니다. 포맷 실패가 있으면 뒤 단계에 도달하지 못한다는 조건도 적습니다.
목표: 순서가 필요한 작업과 결과 재사용이 가능한 작업을 나눕니다. docs/guide/run.md와 docs/config/run.md를 읽고 가상의 문서 사이트 작업을 설계하세요.
문서 메타데이터 생성 -> 사이트 빌드 -> 배포
^
|
링크 검사 완료
command, dependsOn을 정합니다.cache: false를 명시합니다.DOC_LANG이 결과에 영향을 준다면 캐시 입력에 어떻게 넣을지 적습니다.package.json과 run.tasks에 중복 정의하지 않았는지 확인합니다.완료 기준: 순환 없는 의존성 그림과 설정 초안을 제출합니다. command: ['vp', 'build']는 인자 배열이 아니라 두 명령을 순서대로 실행하는 설정이므로, command: 'vp build'와 다름도 설명합니다.
목표: Git 설정을 실제로 수정하지 않고 재시도 정책을 검증하는 모델을 설계합니다. packages/cli/src/config/hooks.ts의 writeGitConfig()와 관련 테스트 세 가지를 읽고, 쓰기·대기·재조회를 가짜 함수로 치환한 의사 코드를 작성하세요.
| 가짜 입력 상황 | 예상 동작 |
|---|---|
| 최초 쓰기 성공 | 대기 없이 종료 |
| 잠금 실패 후 같은 값 확인 | 한 번 기다린 뒤 추가 쓰기 없이 성공 |
| 다른 오류 코드 | 잠금 재시도를 적용하지 않음 |
| 잠금 실패가 계속됨 | 대기 다섯 번, 쓰기 최대 여섯 번 후 실패 반환 |
완료 기준: 각 상황의 쓰기 호출 수, 재조회 횟수, 대기 합계를 적습니다. 대기 함수가 실제 시간을 흘려보내지 않아도 검증할 수 있도록 설계하고, 실제 Git 연동 테스트가 추가로 확인해야 할 부분을 한 문단으로 정리합니다.
주당 3~5시간의 학습을 가정한 예시입니다. Rust가 처음이라면 3주 차를 두 주로 늘려도 좋습니다. 모든 구성 요소를 설치하는 일보다 매주 하나의 설명 가능한 결과물을 남기는 데 집중합니다.
| 주차 | 공부할 내용 | 읽을 소스 | 남길 결과물 |
|---|---|---|---|
| 1주 차 | CLI, package.json, 내장 명령과 스크립트 | README.md, docs/guide/run.md, packages/cli/src/bin.ts | 명령 10개의 실행 대상 표 |
| 2주 차 | TypeScript 설정 타입, 모듈 탐색, 별칭 | packages/cli/src/define-config.ts, packages/cli/src/resolve-test.ts, packages/cli/BUNDLING.md | CLI·코어·Vitest의 연결 그림 |
| 3주 차 | Rust의 enum, Option, Result, match | crates/vp_static_config/src/lib.rs | 정적 분석 결과를 예측하는 입력 8개 |
| 4주 차 | NAPI, 콜백, Promise, 프로세스 실행 | packages/cli/binding/src/lib.rs, crates/vp_global_cli/src/js_executor.rs | 언어 경계·프로세스 경계를 따로 그린 도식 |
| 5주 차 | 검사 순서, 멱등성, 잠금·재시도 | packages/cli/binding/src/check/mod.rs, packages/cli/src/config/hooks.ts | 실패 유형별 복구 안내와 재시도 시간표 |
| 6주 차 | 작업 그래프, 캐시, CI, 네이티브 배포 | docs/config/run.md, .github/workflows/ci.yml, Vite Task 저장소 | 작은 프로젝트의 도구 통합 설계서 |
마지막 목표는 Vite+를 통째로 재구현하는 것이 아니라, 왜 어떤 책임은 Rust에 있고 어떤 책임은 TypeScript에 있는지 자기 말로 설명하는 것입니다.
처음부터 엔진 내부를 분해하기보다 차량의 조작부, 배선, 제어기, 엔진 순서로 살펴보는 방식입니다. 입구와 연결을 이해한 뒤 Vite Task나 Rolldown의 내부로 들어가면, 그 알고리즘이 실제 제품에서 왜 필요한지도 함께 보입니다.
| 용어 | 쉬운 설명 | 이 저장소에서 만나는 곳 |
|---|---|---|
| 도구 모음 | 개발·검사·테스트·빌드를 수행하는 여러 도구의 연결 | vp 명령 체계 |
| 런타임 | 프로그램을 실제로 실행하는 환경 | 관리 대상 Node.js |
| 크레이트 | Rust에서 컴파일·의존성 관리의 단위가 되는 패키지 | vp_static_config, vp_global_cli |
| 워크스페이스 | 관련 패키지를 묶고 공통 설정을 나누는 범위 | Cargo와 pnpm의 루트 설정 |
| 네이티브 모듈 | 운영체제·CPU에 맞춰 기계어로 빌드한 연결 부품 | Node.js가 불러오는 Rust 바인딩 |
| NAPI-RS | Rust 코드를 Node.js의 네이티브 API와 연결하는 도구 | packages/cli/binding/src/lib.rs |
| resolver | 이름이나 설정을 받아 실제 사용할 경로나 값을 찾는 역할 | packages/cli/src/resolve-vite.ts, packages/cli/src/resolve-test.ts |
| 콜백 | 호출할 시점을 다른 코드에 맡겨 전달하는 함수 | Rust에 넘기는 JS 도구 탐색 함수 |
| Promise | 아직 끝나지 않은 작업의 성공·실패 결과를 나타내는 객체 | JS resolver의 비동기 결과 |
| AST | 코드의 문법 구조를 나무 모양 데이터로 바꾼 것 | Oxc Parser의 설정 분석 |
| 정적 분석 | 코드를 실제 실행하지 않고 문법과 구조로 성질을 알아내기 | resolve_static_config() |
| 대체 경로 | 빠른 방법으로 처리하지 못할 때 사용하는 다음 방법 | 동적 설정의 JS 로딩 경로 |
| 열린 맵 | 보이는 키 외에 다른 키도 있을 수 있다고 취급하는 표 | 전개 연산자를 처리하는 FieldMap |
| 모듈 보강 | 기존 모듈의 타입 정의에 필드를 추가하는 TypeScript 방식 | Vite UserConfig 확장 |
| 패키지 별칭 | 의존성 이름과 실제 가져올 패키지 이름을 다르게 연결하기 | vite → Vite+ 코어 |
| 잠금 파일 | 선택된 의존성 버전을 기록해 다시 같은 조합을 쓰도록 돕는 파일 | Cargo.lock, pnpm-lock.yaml |
| 종료 코드 | 프로세스가 끝나면서 남기는 성공·실패 신호 | 검사 결과와 자식 프로세스 상태 |
| 프로세스 | 운영체제가 독립된 실행 단위로 관리하는 프로그램 | 글로벌 Rust CLI, Node.js, 도구 실행 |
| 멱등성 | 같은 요청을 반복해도 최종 상태가 더 달라지지 않는 성질 | 이미 같은 Git 설정이면 쓰지 않기 |
| 경합 | 여러 실행이 같은 자원을 동시에 쓰려는 상황 | worktree 간 config.lock 충돌 |
| 지수형 재시도 | 실패 후 기다리는 시간을 배수로 늘리며 다시 시도하기 | 50 → 100 → 200 → 400 → 800ms |
| 작업 그래프 | 어떤 작업이 끝나야 다음 작업을 할지 연결한 그림 | run.tasks.*.dependsOn |
| 캐시 | 입력이 같을 때 이전 결과를 재사용하는 저장소·규칙 | Vite Task의 작업 결과 재사용 |
| PTY | 프로그램이 실제 터미널처럼 느끼게 해주는 가상 터미널 | vp_cli_snapshots의 상호작용 테스트 |
| shim / trampoline | 사용자 명령을 실제 실행 파일로 연결하는 작은 중간 프로그램 | Node.js·패키지 관리자 연결과 Windows 보조 실행기 |
| 타깃 트리플 | 빌드 결과가 맞아야 할 CPU·운영체제·환경 조합 | 8개 네이티브 배포 대상 |
| 다단계 빌드 | 도구가 많은 빌드 단계와 작은 제품 실행 단계를 분리하기 | docs/guide/docker.md |
영어 식별자를 정확히 기억하는 것도 중요하지만, 코드 리뷰에서는 동작을 문장으로 풀어 말해 보세요. “fallback을 쓴다”보다 “정적으로 값을 읽지 못하면 Node.js에서 설정을 로딩한다”가 이 설계를 더 잘 설명합니다.
| 항목 | 상태 | 기준 |
|---|---|---|
| 저장소 | voidzero-dev/vite-plus | 공식 GitHub |
| 트렌딩 선정 | GitHub Trending Rust Weekly #10 | 2026-10-23 선정 기록 |
| 스타 / 포크 | 6,458 / 296 | 2026-10-23 GitHub 기준 |
| 주 언어 | Rust | GitHub 언어 분류 |
| 생성일 | 2025-03-06 | GitHub 저장소 정보 |
| 최근 push | 2026-10-23 | GitHub 저장소 정보 |
| 최신 릴리스 | v1.4.0 · 2026-10-23 | 기준일의 GitHub 릴리스 정보 |
| 패키지 버전 | CLI·코어·글로벌 CLI 모두 1.4.0 | 각 package.json, crates/vp_global_cli/Cargo.toml |
| 최근 커밋 | d3d9f57 · 2026-10-23 | 공유 Git 설정 잠금을 첫 실행에서 기다리도록 수정 · PR #3199 |
| 추적 경로 수 | 4,949 | git ls-files 기준 |
| 주요 코드 규모 | Rust 약 10.4만 줄 / TypeScript 약 9.5만 줄 | 5절의 확장자별 물리적 줄 수 집계 |
| 네이티브 배포 대상 | 8개 타깃 | packages/cli/package.json 선언 |
| 라이선스 | MIT | 루트 LICENSE, package.json, Cargo 워크스페이스 |
스타·포크·트렌딩·최신 릴리스는 2026-10-23의 스냅샷입니다. 구현 설명은 커밋 d3d9f57 기준이며, 실시간 공식 문서와 이후 버전은 달라질 수 있습니다. 특정 기능이 순위를 올렸다는 인과관계까지 뜻하지는 않습니다.
코드 규모에는 테스트·예제·생성 파일이 포함되며, 외부 Vite·Rolldown·Vite Task의 전체 소스는 빠져 있습니다. 정적 추출기 README의 반환형과 현재 구현이 다르므로, 동작을 판단할 때는 해당 커밋의 함수와 호출자를 함께 확인해야 합니다.
플랫폼 목록과 의존성 버전은 지원 선언·잠금 기록입니다. 모든 플러그인과 프로젝트의 설치 성공이나 성능을 보증하지 않습니다. 캐시는 입력과 환경에 따라 달라지고, 타입 검사는 설정에 따라 활성화됩니다. RAM·디스크 예시는 공식 최소값이 아닙니다.
실습의 짧은 코드와 의사 코드는 설명 범위를 줄인 예시입니다. 실제 도입 시에는 프로젝트의 기존 스크립트, 패키지 별칭, Node.js 버전, 작업 부작용을 확인하고 마이그레이션 결과를 검토하세요.