이 문서는 npm 같은 JavaScript 패키지 매니저를 Rust로
구현할 때 필요한 설계 기준을 정리한 가이드다.
여기서 말하는 대상은 이런 툴이다.
package.json을 읽는다node_modules를 구성한다package-lock.json 같은 lockfile을 쓴다bin 링크를 만든다즉, 일반 바이너리 설치기나 Homebrew류가 아니라, Node 생태계용 dependency manager다.
JavaScript 패키지 매니저는 겉보기보다 훨씬 어렵다. 이유는 다음 때문이다.
dependencies, devDependencies,
peerDependencies, optionalDependencies가 다
다르다node_modules 레이아웃과 hoisting이 복잡하다bin 링크가 있다처음부터 npm 전체를 복제하려고 하면 실패 확률이 높다.
1차 구현은 아래만 지원하는 쪽이 맞다.
package.json 읽기dependencies와 devDependencies 파싱node_modules 설치.bin 링크 생성이 정도면 이미 “작동하는 JS 패키지 매니저”다.
이건 2차 이후가 맞다.
Rust 쪽 패키지 매니저 감각으로 접근하면 자주 틀린다.
package.json프로젝트의 의도된 의존성 선언이다.
공식 npm 문서 기준으로 package.json은 프로젝트
메타데이터와 dependency, script, bin, workspaces 등의 설정을
담는다.
출처: https://docs.npmjs.com/cli/v11/configuring-npm/package-json
처음에 꼭 읽을 필드:
nameversiondependenciesdevDependenciesoptionalDependenciespeerDependenciesbinworkspacesscriptspackage-lock.json은 선언이 아니라 “해결 결과”다.
npm 문서상 lockfile은 설치 트리를 재현 가능하게 만들기 위한
파일이다.
출처:
https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json
즉:
package.json은 원하는 범위package-lock.json은 실제 선택된 정확한 버전과 트리npm registry는 패키지 이름에 대해 여러 버전 메타데이터를 돌려준다. 이 문서를 보통 packument라고 부른다.
npm registry API 문서와 registry 문서 기준으로 패키지 메타데이터는
버전별 정보, dist 정보, tarball URL, integrity, dependencies 등을
담는다.
출처:
node_modules이건 단순한 “패키지 폴더 모음”이 아니다.
Node의 모듈 해석 규칙 때문에 어떤 패키지를 어느 깊이에 두느냐가 동작에 직접 영향을 준다.
즉 JS 패키지 매니저의 핵심은 사실상:
node_modules 배치 전략이 3개다.
추천 구조:
src/
main.rs
lib.rs
cli/
mod.rs
args.rs
app/
mod.rs
install.rs
remove.rs
update.rs
ci.rs
manifest/
mod.rs
package_json.rs
lockfile.rs
registry/
mod.rs
npm.rs
metadata.rs
semver/
mod.rs
resolver/
mod.rs
graph.rs
hoist.rs
store/
mod.rs
cache.rs
extract.rs
tree.rs
bin.rs
scripts/
mod.rs
report/
mod.rs
error.rs
tests/
fixtures/
integration/
각 계층 역할은 이렇다.
install, add, remove,
update, cipackage.json 타입node_modules 구성.bin 링크 생성여기서 많이 실수한다.
Rust semver crate는 Cargo 해석 기준이다. docs.rs
설명에도 이 crate는 Cargo의 semver 해석을 따른다고 되어 있고, 다른
생태계에는 적절치 않을 수 있다고 나온다.
출처: https://docs.rs/semver
반면 node-semver 호환용 Rust crate는 별도로 있다.
node_semver는 Node/NPM의 semver와 호환되도록 설계됐다고
docs.rs에 명시되어 있다.
출처: https://docs.rs/node-semver
JS 패키지 매니저를 만들면:
semver crate를 기본 선택지로 보면 안 된다node_semver 같은 npm 호환 range 파서를 쓰는 쪽이
맞다npm range에는 이런 게 많다.
^1.2.3~1.2.31.x*>=1 <2이걸 Cargo식 해석으로 처리하면 resolver가 틀어진다.
package.json 데이터
모델처음부터 모든 필드를 완벽히 모델링할 필요는 없다. 하지만 너무 적게 잡아도 안 된다.
추천 구조:
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageJson {
pub name: Option<String>,
pub version: Option<String>,
#[serde(default)]
pub dependencies: BTreeMap<String, String>,
#[serde(default, rename = "devDependencies")]
pub dev_dependencies: BTreeMap<String, String>,
#[serde(default, rename = "optionalDependencies")]
pub optional_dependencies: BTreeMap<String, String>,
#[serde(default, rename = "peerDependencies")]
pub peer_dependencies: BTreeMap<String, String>,
#[serde(default)]
pub bin: serde_json::Value,
#[serde(default)]
pub scripts: BTreeMap<String, String>,
#[serde(default)]
pub workspaces: Option<serde_json::Value>,
}직접 struct를 만드는 방법도 좋고, package_json crate를
쓸 수도 있다. docs.rs 기준 package_json은 npm
package.json 스키마에 맞는 타입을 제공한다.
출처:
https://docs.rs/package-json/latest/package_json/struct.PackageJson.html
추천은 이렇다.
package_json실제로는 직접 struct 정의가 유지보수에 더 나은 경우가 많다.
기본적으로 필요한 건 2개다.
핵심 요청:
GET https://registry.npmjs.org/<package-name>이 응답에는 보통:
같은 정보가 들어 있다.
관련 공식 문서:
버전 metadata의 dist.tarball URL로 내려받는다.
설치 플로우는 보통 이렇다.
dist.tarball URL 확인package/ 폴더 내용 설치npm 계열에서는 checksum보다 SRI 문자열을 자주 본다.
예시:
sha512-BASE64...
Rust에선 ssri crate가 매우 적합하다. docs.rs 설명 그대로
SRI 문자열 파싱, 생성, 검증을 지원한다.
출처: https://docs.rs/ssri
ssri를 추천하나반드시 아래 순서여야 한다.
node_modules 반영검증 전 내용을 설치 트리에 노출하면 안 된다.
npm tarball은 일반적으로 압축을 풀면 내부에 package/
디렉터리 아래 파일들이 들어 있다.
즉 설치 시에는 보통:
package/ 폴더를 실제 패키지 루트로 간주이 흐름이 된다.
package.json 존재 여부 확인JavaScript 패키지 매니저는 생각보다 압축 해제 취약점에 민감하다.
lockfile은 설치 결과물의 스냅샷이다.
#[derive(Debug, Serialize, Deserialize)]
pub struct Lockfile {
pub name: Option<String>,
pub version: Option<String>,
#[serde(rename = "lockfileVersion")]
pub lockfile_version: u32,
pub packages: std::collections::BTreeMap<String, LockedPackage>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LockedPackage {
pub version: String,
pub resolved: Option<String>,
pub integrity: Option<String>,
#[serde(default)]
pub dependencies: std::collections::BTreeMap<String, String>,
}이건 npm lockfile과 완전히 같을 필요는 없다. 하지만 최소한 아래는 보장해야 한다.
install과
ci를 분리하는 이유권장 정책:
install: package.json 기준으로 resolve하고
lockfile 갱신 가능ci: lockfile을 엄격히 따르고, 불일치 시 실패이건 npm의 install / ci 역할 분리와 같은
방향이다.
JS 패키지 매니저의 핵심 난이도는 resolver다.
resolver는 단순히 “버전 하나 선택”이 아니다.
처음엔 이 정도가 적당하다.
dependencies 읽기즉 처음부터 pnpm급 최적화를 노리지 말고, “정확한 트리”를 먼저 만드는 게 맞다.
그래프 자료구조가 있으면 편하다.
petgraph는 그래프 표현과 알고리즘에 유용하다.
출처: https://docs.rs/petgraph/latest/petgraph/
하지만 꼭 필요한 건 아니다. 트리 중심 구현이면 직접 구조체로도 충분하다.
예시:
pub struct ResolvedNode {
pub name: String,
pub version: String,
pub integrity: Option<String>,
pub tarball_url: String,
pub dependencies: Vec<ResolvedNode>,
}처음엔 이게 더 단순하다.
node_modules 배치
전략이게 실제 동작을 결정한다.
중첩 설치:
node_modules/
a/
package.json
node_modules/
b/
장점:
단점:
간단한 최적화로 아래를 할 수 있다.
이 정도만 해도 체감 품질이 올라간다.
완전 hoisting은 충돌 해결, peer dependency, bin 경로 모두에 영향을 준다.
1차 버전에선:
정도로 끝내는 게 안전하다.
.bin 링크는 꼭
필요하다많은 JS 패키지가 CLI 실행 파일을 bin 필드로
노출한다.
package_json 문서에도 bin 필드가 npm에서
executable 설치에 사용된다고 설명되어 있다.
출처:
https://docs.rs/package-json/latest/package_json/struct.PackageJson.html
패키지 설치 후:
node_modules/.bin/<name> 생성bin 스크립트Unix에서는 symlink가 흔하고, Windows는 .cmd shim이
필요할 수 있다.
bin이 string일 수도 있고 object일 수도 있다JS 패키지 매니저에서 lifecycle script는 큰 복잡도를 만든다.
예시:
preinstallinstallpostinstallprepare이건 사실상 arbitrary code execution이다.
선택지 3개 중 하나를 고르는 게 좋다.
--ignore-scripts 기본값 true보안과 디버깅 관점에서 초반에는 이게 맞다.
필요한 것:
.bin 주입이건 설치기보다 “프로세스 실행 플랫폼”에 가까워진다.
npm 문서 기준 workspaces는 하나의 상위 프로젝트 안에 여러 패키지를
두고, 설치 시 자동 symlink되는 구조다.
출처: https://docs.npmjs.com/cli/v8/using-npm/workspaces/
workspaces 필드를 파싱만 하거나예시:
pub enum ProjectKind {
SinglePackage,
Workspace { members: Vec<std::path::PathBuf> },
}나중에 workspace를 붙일 가능성이 매우 높기 때문이다.
여기서는 “JS 패키지 매니저” 기준으로 추천한다.
| crate | 용도 | 이유 |
|---|---|---|
clap |
CLI 파싱 | 서브커맨드, help, 에러 메시지 품질이 좋다 |
thiserror |
typed error | 내부 에러 모델 정리에 적합 |
serde |
JSON/TOML 직렬화 | package.json, lockfile, registry metadata 처리 |
serde_json |
JSON 파싱 | package.json과 registry 응답의 핵심 |
reqwest |
HTTP 클라이언트 | registry 요청과 tarball 다운로드 |
node_semver |
npm semver 해석 | Node/NPM 호환 range 처리 |
ssri |
integrity 검증 | npm dist integrity와 직접 맞는다 |
tempfile |
임시 디렉터리 | tarball staging, atomic install |
tar |
tarball 해제 | npm package tarball 처리 |
flate2 |
gzip 해제 | .tgz 대응 |
url |
URL 처리 | registry URL, resolved URL 정규화 |
| crate | 용도 | 이유 |
|---|---|---|
package_json |
package.json 타입 |
빠르게 시작하기 좋다 |
camino |
UTF-8 path | JS 툴링에서는 문자열 path 취급이 많아서 편하다 |
fs-err |
나은 fs 에러 | 어떤 파일 작업이 실패했는지 더 잘 나온다 |
fd-lock |
파일 lock | lockfile/state 동시성 제어 |
indicatif |
진행 표시 | install UX 개선 |
tracing |
verbose/debug 로깅 | resolver와 install 디버깅에 유용 |
ignore |
파일 무시 규칙 | pack/publish, workspace 스캔, 파일 트리 처리 |
globset |
glob 매칭 | workspace, files whitelist 처리 |
| crate | 용도 | 메모 |
|---|---|---|
petgraph |
dependency graph | 복잡한 resolver를 만들 때 유용 |
tokio |
async 다운로드 | 병렬 fetch가 필요하면 도입 |
miette |
rich diagnostic | CLI 진단형 에러를 강화할 때 |
sha2 |
fallback hash | integrity 외 별도 checksum 정책이 필요할 때 |
[dependencies]
clap = { version = "4", features = ["derive"] }
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"] }
node-semver = "2"
ssri = "9"
tempfile = "3"
tar = "0.4"
flate2 = "1"
url = "2"
fs-err = "3"
fd-lock = "4"
indicatif = "0.18"
tracing = "0.1"
tracing-subscriber = "0.3"특징:
[dependencies]
clap = { version = "4", features = ["derive"] }
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
node-semver = "2"
ssri = "9"
tempfile = "3"
tar = "0.4"
flate2 = "1"
url = "2"
camino = "1"
fs-err = "3"
fd-lock = "4"
ignore = "0.4"
globset = "0.4"
petgraph = "0.8"
indicatif = "0.18"
tracing = "0.1"
tracing-subscriber = "0.3"
miette = "7"특징:
install은 아래 순서가 안전하다.
package.json 읽기node_modules 반영.bin 생성node_modules를 직접 건드리지 말 것추천 방식:
AppError, ResolveError,
RegistryError, InstallError 같은 typed
errorReport로 요약 출력예시:
pub struct Report {
pub summary: String,
pub details: Vec<String>,
}좋은 에러 메시지는 아래를 포함해야 한다.
JS 패키지 매니저에서는 그냥 failed to install 정도로는
아무 의미가 없다.
^~x*.bin 생성실전에서는 테스트용 로컬 registry fixture가 거의 필수다.
추천 방식:
.tgz fixturepackage.json 파서node_modules 설치.bin 생성install / ci이 순서가 제일 안전하다.
Rust로 npm 같은 JS 패키지 매니저를 만들 때 제일 중요한 건 이거다.
node_semver와 ssri 같은 npm 친화 crate를
쓸 것node_modules 배치 전략을 핵심 문제로 볼
것한 줄로 요약하면:
이 프로젝트의 본질은 “다운로드 툴”이 아니라 “npm registry metadata를
해석해서 정확한 node_modules 트리를 재현하는 엔진”이다.
공식 npm 문서:
package.json:
https://docs.npmjs.com/cli/v11/configuring-npm/package-jsonpackage-lock.json:
https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-jsonregistry:
https://docs.npmjs.com/cli/v8/using-npm/registry/workspaces:
https://docs.npmjs.com/cli/v8/using-npm/workspaces/npm Registry API: https://api-docs.npmjs.com/추천 Rust crate 문서:
clap: https://docs.rs/crate/clap/latestthiserror: https://docs.rs/crate/thiserror/latestserde: https://docs.rs/serde/latest/serdeserde_json:
https://docs.rs/serde_json/latest/serde_json/reqwest: https://docs.rs/reqwest/node_semver: https://docs.rs/node-semverssri: https://docs.rs/ssripackage_json:
https://docs.rs/package-json/latest/package_json/struct.PackageJson.htmltempfile: https://docs.rs/crate/tempfile/latesttar: https://docs.rs/crate/tar/latestflate2: https://docs.rs/crate/flate2/latesturl: https://docs.rs/crate/url/latestfs-err: https://docs.rs/crate/fs-err/latestfd-lock: https://docs.rs/fd-lockignore: https://docs.rs/ignore/latest/ignore/globset: https://docs.rs/globset/latest/globset/petgraph:
https://docs.rs/petgraph/latest/petgraph/indicatif: https://docs.rs/indicatiftracing: https://docs.rs/tracing/latest/tracing/miette: https://docs.rs/crate/miette/latest