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

Filter by extension

Filter by extension


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

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

5 changes: 4 additions & 1 deletion crates/vite_install/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ pub mod package_manager;
mod request;
mod shim;

pub use package_manager::{PackageManager, PackageManagerType};
pub use package_manager::{
PackageManager, PackageManagerType, download_package_manager,
get_package_manager_type_and_version,
};
88 changes: 40 additions & 48 deletions crates/vite_install/src/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,45 +92,23 @@ impl PackageManagerBuilder {
/// Detect the package manager from the current working directory.
pub async fn build(self) -> Result<PackageManager, Error> {
let workspace_root = find_workspace_root(&self.cwd)?;
let (package_manager_type, mut version, mut hash) =
let (package_manager_type, version_or_latest, hash) =
get_package_manager_type_and_version(&workspace_root, self.client_override)?;

let mut package_name = package_manager_type.to_string();
let mut should_update_package_manager_field = false;

if version == "latest" {
version = get_latest_version(package_manager_type).await?;
should_update_package_manager_field = true;
hash = None; // Reset hash when fetching latest since hash is version-specific
}

// handle yarn >= 2.0.0 to use `@yarnpkg/cli-dist` as package name
// @see https://github.com/nodejs/corepack/blob/main/config.json#L135
if matches!(package_manager_type, PackageManagerType::Yarn) {
let version_req = VersionReq::parse(">=2.0.0")?;
if version_req.matches(&Version::parse(&version)?) {
package_name = "@yarnpkg/cli-dist".to_string();
}
}

// only download the package manager if it's not already downloaded
let install_dir = download_package_manager(
package_manager_type,
&package_name,
&version,
hash.as_deref(),
)
.await?;
let (install_dir, package_name, version) =
download_package_manager(package_manager_type, &version_or_latest, hash.as_deref())
.await?;

if should_update_package_manager_field {
if version_or_latest != version {
// auto set `packageManager` field in package.json
let package_json_path = workspace_root.path.join("package.json");
set_package_manager_field(&package_json_path, package_manager_type, &version).await?;
}

Ok(PackageManager {
client: package_manager_type,
package_name: package_name.into(),
package_name,
version,
hash,
bin_name: package_manager_type.to_string().into(),
Expand Down Expand Up @@ -200,7 +178,7 @@ impl PackageManager {
}

/// Get the package manager name, version and optional hash from the workspace root.
fn get_package_manager_type_and_version(
pub fn get_package_manager_type_and_version(
workspace_root: &WorkspaceRoot,
default: Option<PackageManagerType>,
) -> Result<(PackageManagerType, Str, Option<Str>), Error> {
Expand Down Expand Up @@ -321,17 +299,32 @@ async fn get_latest_version(package_manager_type: PackageManagerType) -> Result<

/// Download the package manager and extract it to the cache directory.
/// Return the install directory, e.g. $`CACHE_DIR/vite/package_manager/pnpm/10.0.0/pnpm`
async fn download_package_manager(
pub async fn download_package_manager(
package_manager_type: PackageManagerType,
package_name: &str,
version: &str,
version_or_latest: &str,
expected_hash: Option<&str>,
) -> Result<AbsolutePathBuf, Error> {
let tgz_url = get_npm_package_tgz_url(package_name, version);
) -> Result<(AbsolutePathBuf, Str, Str), Error> {
let version: Str = if version_or_latest == "latest" {
get_latest_version(package_manager_type).await?
} else {
version_or_latest.into()
};

let mut package_name: Str = package_manager_type.to_string().into();
// handle yarn >= 2.0.0 to use `@yarnpkg/cli-dist` as package name
// @see https://github.com/nodejs/corepack/blob/main/config.json#L135
if matches!(package_manager_type, PackageManagerType::Yarn) {
let version_req = VersionReq::parse(">=2.0.0")?;
if version_req.matches(&Version::parse(&version)?) {
package_name = "@yarnpkg/cli-dist".into();
}
}

let tgz_url = get_npm_package_tgz_url(&package_name, &version);
let cache_dir = get_cache_dir()?;
let bin_name = package_manager_type.to_string();
// $CACHE_DIR/vite/package_manager/pnpm/10.0.0
let target_dir = cache_dir.join("package_manager").join(&bin_name).join(version);
let target_dir = cache_dir.join("package_manager").join(&bin_name).join(&version);
let install_dir = target_dir.join(&bin_name);

// If all shims are already exists, return the target directory
Expand All @@ -342,7 +335,7 @@ async fn download_package_manager(
&& is_exists_file(bin_file.with_extension("cmd"))?
&& is_exists_file(bin_file.with_extension("ps1"))?
{
return Ok(install_dir);
return Ok((install_dir, package_name, version));
}

// $CACHE_DIR/vite/package_manager/pnpm/{tmp_name}
Expand All @@ -360,7 +353,7 @@ async fn download_package_manager(
{
Error::PackageManagerVersionNotFound {
name: package_manager_type.to_string().into(),
version: version.into(),
version: version.clone(),
url: tgz_url.into(),
}
} else {
Expand Down Expand Up @@ -388,7 +381,7 @@ async fn download_package_manager(
// the installation while we were downloading
if is_exists_file(&bin_file)? {
tracing::debug!("bin_file already exists after lock acquisition, skip rename");
return Ok(install_dir);
return Ok((install_dir, package_name, version));
}

// rename $target_dir_tmp to $target_dir
Expand All @@ -400,7 +393,7 @@ async fn download_package_manager(
tracing::debug!("Create shim files for {}", bin_name);
create_shim_files(package_manager_type, &bin_prefix).await?;

Ok(install_dir)
Ok((install_dir, package_name, version))
}

/// Remove the directory and all its contents.
Expand Down Expand Up @@ -1365,24 +1358,23 @@ mod tests {

#[tokio::test]
async fn test_download_package_manager() {
let result =
download_package_manager(PackageManagerType::Yarn, "@yarnpkg/cli-dist", "4.9.2", None)
.await;
let result = download_package_manager(PackageManagerType::Yarn, "4.9.2", None).await;
assert!(result.is_ok());
let target_dir = result.unwrap();
let (target_dir, package_name, version) = result.unwrap();
println!("result: {target_dir:?}");
assert!(is_exists_file(target_dir.join("bin/yarn")).unwrap());
assert!(is_exists_file(target_dir.join("bin/yarn.cmd")).unwrap());
assert_eq!(package_name, "@yarnpkg/cli-dist");
assert_eq!(version, "4.9.2");

// again should skip download
let result =
download_package_manager(PackageManagerType::Yarn, "@yarnpkg/cli-dist", "4.9.2", None)
.await;
let result = download_package_manager(PackageManagerType::Yarn, "4.9.2", None).await;
assert!(result.is_ok());
let target_dir = result.unwrap();
let (target_dir, package_name, version) = result.unwrap();
assert!(is_exists_file(target_dir.join("bin/yarn")).unwrap());
assert!(is_exists_file(target_dir.join("bin/yarn.cmd")).unwrap());

assert_eq!(package_name, "@yarnpkg/cli-dist");
assert_eq!(version, "4.9.2");
remove_dir_all_force(target_dir).await.unwrap();
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ vite_install = { workspace = true }
vite_path = { workspace = true }
vite_str = { workspace = true }
vite_task = { workspace = true }
vite_workspace = { workspace = true }

[build-dependencies]
napi-build = { workspace = true }
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/binding/__tests__/detect-workspace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { tmpdir } from 'node:os';
import path from 'node:path';

import { expect, test } from 'vitest';

import { detectWorkspace } from '../index.js';

const fixtures = path.join(import.meta.dirname, 'fixtures');

test('should detect pnpm monorepo workspace successfully', async () => {
const cwd = path.join(fixtures, 'pnpm-monorepo');
const result = await detectWorkspace(cwd);
expect(result.packageManagerName).toBe('pnpm');
expect(result.packageManagerVersion).toBe('10.19.0');
expect(result.isMonorepo).toBe(true);
expect(result.root).toBe(cwd);

// detect from sub directory
const subCwd = path.join(cwd, 'packages', 'sub-package');
const subResult = await detectWorkspace(subCwd);
expect(subResult.packageManagerName).toBe('pnpm');
expect(subResult.packageManagerVersion).toBe('10.19.0');
expect(subResult.isMonorepo).toBe(true);
expect(subResult.root).toBe(cwd);
});

test('should detect npm monorepo workspace successfully', async () => {
const cwd = path.join(fixtures, 'npm-monorepo');
const result = await detectWorkspace(cwd);
expect(result.packageManagerName).toBe('npm');
expect(result.packageManagerVersion).toBe('10.19.0');
expect(result.isMonorepo).toBe(true);
expect(result.root).toBe(cwd);
});

// FIXME: currently it will always find vite-plus, there is a problem here
test.skip('should detect npm project successfully', async () => {
const cwd = path.join(fixtures, 'npm-project');
const result = await detectWorkspace(cwd);
expect(result.packageManagerName).toBe('npm');
expect(result.packageManagerVersion).toBe('10.19.0');
expect(result.isMonorepo).toBe(false);
expect(result.root).toBe(cwd);
});

test('should detect workspace failed with not exists directory', async () => {
const result = await detectWorkspace(path.join(tmpdir(), 'not-exists'));
expect(result.packageManagerName).toBeUndefined();
expect(result.packageManagerVersion).toBeUndefined();
expect(result.isMonorepo).toBe(false);
expect(result.root).toBeUndefined();
});
15 changes: 15 additions & 0 deletions packages/cli/binding/__tests__/download-package-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from 'vitest';

import { downloadPackageManager } from '../index.js';

test('should download package manager successfully', async () => {
const result = await downloadPackageManager({
name: 'pnpm',
version: 'latest',
});
expect(result.name).toBe('pnpm');
expect(result.packageName).toBe('pnpm');
expect(result.version).toMatch(/^\d+\.\d+\.\d+$/);
expect(result.installDir).toBeTruthy();
expect(result.binPrefix).toMatch(/bin$/);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "npm-monorepo",
"version": "1.0.0",
"packageManager": "npm@10.19.0",
"workspaces": [
"packages/*"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "npm-project",
"version": "1.0.0",
"packageManager": "npm@10.19.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "pnpm-monorepo",
"version": "1.0.0",
"packageManager": "pnpm@10.19.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "pnpm-monorepo-foo"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- packages/*
83 changes: 83 additions & 0 deletions packages/cli/binding/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,89 @@ export interface CliOptions {
resolveUniversalViteConfig: ((err: Error | null, arg: string) => Promise<string>)
}

/**
* Detect the workspace root and package manager type and version
*
* ## Parameters
*
* - `cwd`: The current working directory to detect the workspace root
*
* ## Returns
*
* Returns a `DetectWorkspaceResult` containing:
* - The name of the package manager
* - The version of the package manager
* - Whether the workspace is a monorepo
* - The workspace root, where the package.json file is located.
*
* ## Example
*
* ```javascript
* const result = await detectWorkspace("/path/to/workspace");
* console.log(`Package manager name: ${result.package_manager_name}`);
* console.log(`Package manager version: ${result.package_manager_version}`);
* console.log(`Is monorepo: ${result.is_monorepo}`);
* console.log(`Workspace root: ${result.root}`);
* ```
*/
export declare function detectWorkspace(cwd: string): Promise<DetectWorkspaceResult>

export interface DetectWorkspaceResult {
packageManagerName?: string
packageManagerVersion?: string
isMonorepo: boolean
root?: string
}

/**
* Download a package manager
*
* ## Parameters
*
* - `options`: Configuration for the package manager to download, including:
* - `name`: The name of the package manager
* - `version`: The version of the package manager
* - `expected_hash`: The expected hash of the package manager
*
* ## Returns
*
* Returns a `DownloadPackageManagerResult` containing:
* - The name of the package manager
* - The install directory of the package manager
* - The binary prefix of the package manager
* - The package name of the package manager
* - The version of the package manager
*
* ## Example
*
* ```javascript
* const result = await downloadPackageManager({
* name: "pnpm",
* version: "latest",
* });
* console.log(`Package manager name: ${result.name}`);
* console.log(`Package manager install directory: ${result.install_dir}`);
* console.log(`Package manager binary prefix: ${result.bin_prefix}`);
* console.log(`Package manager package name: ${result.package_name}`);
* console.log(`Package manager version: ${result.version}`);
* ```
*/
export declare function downloadPackageManager(options: DownloadPackageManagerOptions): Promise<DownloadPackageManagerResult>

export interface DownloadPackageManagerOptions {
name: string
version: string
expectedHash?: string
}

export interface DownloadPackageManagerResult {
name: string
installDir: string
binPrefix: string
packageName: string
version: string
}

/**
* Result returned by JavaScript resolver functions.
*
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/binding/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { run, runCommand } = nativeBinding
const { detectWorkspace, downloadPackageManager, run, runCommand } = nativeBinding
export { detectWorkspace }
export { downloadPackageManager }
export { run }
export { runCommand }
Loading
Loading