パッケージマネージャ

面白そうなのでやってみようかな (途中で気持ちが折れなければ...)

やること
- package.json から dependencies / devDependencies を読み取る
- production オプションを指定すると devDependencies は無視される
- パッケージを指定してインストールコマンドを実行するとそのパッケージをpackage.jsonに追加する
- lockファイルを作成する
- セマンティックバージョンのインストール
- 依存パッケージの再帰的な解決
- consoleにログを出力

npmのmanifestをダウンロードして解凍するところで詰まった。
npmのtarball
を解凍すると、root階層が package
ディレクトリになっているっぽい。
が、/node_modules
に展開する時 package
は含めず、その配下にあるモジュールを配置する。
この対応は strip_prefix
というメソッドを使うことで解決できた。
もう1つ色々試行錯誤したのがダウンロードしたフォルダの解凍。
archive
の unpack
をする時、対象のディレクトリが存在しないと解凍ファイルを展開してくれない。
そのため、先に対象となるディレクトリを作成しておく必要がある。
当たり前だけどnpmのパッケージはそれぞれフォルダ構成が異なるので、階層が深い場合はその深さまでディレクトリを作成する必要がある。
と、いうので結構汚くなってしまった。。。
async fn install_npm_manifest(
package_name: &str,
version: &Version,
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
const REGISTRY: &str = "https://registry.npmjs.org/";
let url = Url::parse(&format!("{}/{}", REGISTRY, package_name))?;
let manifest: NpmManifest = client.get(url).send().await?.json().await?;
let tarball_url = &manifest.versions[version].dist.tarball;
let res = client.get(tarball_url).send().await?.bytes().await?;
let mut archive = Archive::new(GzDecoder::new(res.as_ref()));
const PREFIX: &str = "package";
fs::create_dir_all(format!("./node_modules/{}", package_name))?;
archive
.entries()?
.filter_map(|e| e.ok())
.map(|mut entry| -> Result<PathBuf, Box<dyn std::error::Error>> {
let path_buf = entry.path()?.strip_prefix(&PREFIX)?.to_owned();
// 先に解凍したファイルを展開するためのディレクトリを作っておく必要がある
let binding = path_buf.display().to_string();
let mut splitted_paths = binding.split("/").collect::<Vec<&str>>();
if splitted_paths.len() > 1 {
splitted_paths.pop();
let path = format!(
"./node_modules/{}/{}",
package_name,
splitted_paths.join("/")
);
fs::create_dir(&path)?;
}
fs::create_dir_all(format!(
"./node_modules/{}/{}",
package_name,
path_buf.display()
))?;
// ここまで
entry.unpack(format!(
"./node_modules/{}/{}",
package_name,
path_buf.display()
))?;
Ok(path_buf)
})
.filter_map(|e| e.ok())
.for_each(|x| println!("> {}", x.display()));
Ok(())
}
PathBuf
struct に生えている parent
というメソッドを使えば、リーフノードから見て親に当たるディレクトリを全て取得できることが分かったので、スッキリした。
let prefix = path_buf.parent().unwrap();
fs::create_dir_all(format!(
"./node_modules/{}/{}",
package_name,
prefix.display()
))?;

とりあえずトップレベルのパッケージを1つだけインストールすることはできるようになった。
依存先のパッケージとかは無理。少しコード整理。

semantic versionからインストール対象のバージョンをどうやって決定するか。
nodejs_semver
というクレートがあったので利用する。これ自作するのは結構めんどくさいからありがたい...!
pub type Version = nodejs_semver::Version;
pub type Range = nodejs_semver::Range;
pub type Constraint = String;
pub fn max_satisfying(versions: &Vec<Version>, constraint: Constraint) -> &Version {
let range: Range = constraint.parse().unwrap();
range.max_satisfying(versions).unwrap()
}

依存パッケージを再帰的に取れるようにした。
pub async fn resolve(root_manifest: PackageJson) -> Result<Info, Box<dyn std::error::Error>> {
let mut top_level: TopLevel = HashMap::new();
let mut npm_manifest_cache: HashMap<String, Manifest> = HashMap::new();
if !root_manifest.dependencies.is_empty() {
for (package_name, version) in root_manifest.dependencies {
resolve_package_recursively(
package_name,
version,
&mut top_level,
&mut npm_manifest_cache,
)
.await?;
}
}
// TODO: dev_dependencies
return Ok(Info { top_level });
}
#[async_recursion]
async fn resolve_package_recursively(
name: String,
constraint: String,
top_level: &mut TopLevel,
npm_manifest_cache: &mut HashMap<String, Manifest>,
) -> Result<(), Box<dyn std::error::Error>> {
let manifest = installer::fetch_manifest(name.clone(), npm_manifest_cache).await?;
let version = semver_max_satisfying(manifest.clone(), constraint.clone()).await?;
println!(
"[Root Resolve by manifest] {}@{} to {}",
&name, constraint, &version
);
let matched_manifest = manifest.get(&version.to_string()).unwrap().clone();
if top_level.get(&name).is_none() {
top_level.insert(
name,
TopLevelDetail {
url: matched_manifest.dist.tarball,
version: version.to_string(),
},
);
}
if let Some(dependencies) = matched_manifest.dependencies {
for (name, constraint) in dependencies {
resolve_package_recursively(name, constraint, top_level, npm_manifest_cache).await?;
}
}
Ok(())
}
async fn semver_max_satisfying(
manifest: Manifest,
constraint: String,
) -> Result<npm_semver::Version, Box<dyn std::error::Error>> {
let versions: Vec<nodejs_semver::Version> = manifest
.iter()
.map(|(version, _)| version.parse().unwrap())
.collect();
let max_satisfying = npm_semver::max_satisfying(versions, constraint);
Ok(max_satisfying)
}
async function を再帰で使うとエラーで怒られる。async_recursion
クレートを使うことでこれを解消できるっぽい、この辺は後でちゃんと読みたいな。
viteインストールして起動したら動いた...!
node_modules/vite/bin/vite.js

キャッシュを取る。
Fetch manifest https://registry.npmjs.org/vue
[Root Resolve by manifest] vue@^3.2.37 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/shared
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/compiler-dom
[Resolve by manifest] @vue/compiler-dom@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/server-renderer
[Resolve by manifest] @vue/server-renderer@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/runtime-dom
[Resolve by manifest] @vue/runtime-dom@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/compiler-sfc
[Resolve by manifest] @vue/compiler-sfc@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/postcss
[Resolve by manifest] postcss@^8.4.48 to 8.4.49
Fetch manifest https://registry.npmjs.org/@vue/compiler-core
[Resolve by manifest] @vue/compiler-core@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-dom@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/estree-walker
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
Fetch manifest https://registry.npmjs.org/magic-string
[Resolve by manifest] magic-string@^0.30.11 to 0.30.14
Fetch manifest https://registry.npmjs.org/@babel/parser
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/compiler-ssr
[Resolve by manifest] @vue/compiler-ssr@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/source-map-js
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] @vue/compiler-dom@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-core@3.5.13 to 3.5.13
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
Fetch manifest https://registry.npmjs.org/entities
[Resolve by manifest] entities@^4.5.0 to 4.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@babel/types
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
Fetch manifest https://registry.npmjs.org/@babel/helper-string-parser
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
Fetch manifest https://registry.npmjs.org/@babel/helper-validator-identifier
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
Fetch manifest https://registry.npmjs.org/@jridgewell/sourcemap-codec
[Resolve by manifest] @jridgewell/sourcemap-codec@^1.5.0 to 1.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-core@3.5.13 to 3.5.13
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
[Resolve by manifest] entities@^4.5.0 to 4.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
[Resolve by manifest] entities@^4.5.0 to 4.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
Fetch manifest https://registry.npmjs.org/picocolors
[Resolve by manifest] picocolors@^1.1.1 to 1.1.1
[Resolve by manifest] source-map-js@^1.2.1 to 1.2.1
Fetch manifest https://registry.npmjs.org/nanoid
[Resolve by manifest] nanoid@^3.3.7 to 3.3.8
Fetch manifest https://registry.npmjs.org/csstype
[Resolve by manifest] csstype@^3.1.3 to 3.1.3
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/reactivity
[Resolve by manifest] @vue/reactivity@3.5.13 to 3.5.13
Fetch manifest https://registry.npmjs.org/@vue/runtime-core
[Resolve by manifest] @vue/runtime-core@3.5.13 to 3.5.13
[Resolve by manifest] @vue/reactivity@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-ssr@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-dom@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-core@3.5.13 to 3.5.13
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
[Resolve by manifest] entities@^4.5.0 to 4.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @vue/compiler-core@3.5.13 to 3.5.13
[Resolve by manifest] @babel/parser@^7.25.3 to 7.26.2
[Resolve by manifest] source-map-js@^1.2.0 to 1.2.1
[Resolve by manifest] estree-walker@^2.0.2 to 2.0.2
[Resolve by manifest] entities@^4.5.0 to 4.5.0
[Resolve by manifest] @vue/shared@3.5.13 to 3.5.13
[Resolve by manifest] @babel/types@^7.26.0 to 7.26.0
[Resolve by manifest] @babel/helper-string-parser@^7.25.9 to 7.25.9
[Resolve by manifest] @babel/helper-validator-identifier@^7.25.9 to 7.25.9
キャッシュできてそうやけど、コードが汚くなってきた。。。

async function 直列で実行してるので遅いんだけど、ここの並列化はかなり重そうなので今回は端折りたい。
別の機会に勉強する。