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 packages/cli/binding/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ module.exports.detectWorkspace = nativeBinding.detectWorkspace;
module.exports.documentationUrl = nativeBinding.documentationUrl;
module.exports.downloadPackageManager = nativeBinding.downloadPackageManager;
module.exports.ensureBlockingStdio = nativeBinding.ensureBlockingStdio;
module.exports.findMonorepoRoot = nativeBinding.findMonorepoRoot;
module.exports.getVpDirs = nativeBinding.getVpDirs;
module.exports.hasConfigKey = nativeBinding.hasConfigKey;
module.exports.isDirectoryGitignored = nativeBinding.isDirectoryGitignored;
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/binding/index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -3696,6 +3696,13 @@ export interface DownloadPackageManagerResult {
/** Re-enable blocking stdio after Node.js has initialized its lazy standard streams. */
export declare function ensureBlockingStdio(): void;

/**
* Find the monorepo root containing `cwd` with the same rules as `vp run` and `vp install`.
*
* Returns `null` when `cwd` is not inside a pnpm or `package.json` workspace.
*/
export declare function findMonorepoRoot(cwd: string): string | null;

/**
* Resolved on-disk category roots from [`vp_shared::EnvConfig`].
*
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/binding/src/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,24 @@ pub async fn detect_workspace(cwd: String) -> Result<DetectWorkspaceResult> {
}
}

/// Find the monorepo root containing `cwd` with the same rules as `vp run` and `vp install`.
///
/// Returns `null` when `cwd` is not inside a pnpm or `package.json` workspace.
#[napi]
pub fn find_monorepo_root(cwd: String) -> Result<Option<String>> {
Comment thread
fengmk2 marked this conversation as resolved.
let cwd = AbsolutePathBuf::new(cwd.into()).ok_or(Error::from_reason("invalid cwd"))?;
match find_workspace_root(&cwd) {
Ok((workspace_root, _)) => match workspace_root.workspace_file {
WorkspaceFile::PnpmWorkspaceYaml(_) | WorkspaceFile::NpmWorkspaceJson(_) => {
Ok(Some(workspace_root.path.as_path().to_string_lossy().to_string()))
}
WorkspaceFile::NonWorkspacePackage(_) => Ok(None),
},
Err(PackageJsonNotFound(_)) => Ok(None),
Err(e) => Err(anyhow::Error::from(e).into()),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/__tests__/resolve-vite-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ describe('findViteConfigUp', () => {
expect(result).toBeUndefined();
});

it('should not search above stopDir when starting from its parent', () => {
fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), '');
const stopDir = path.join(tempDir, 'monorepo');
fs.mkdirSync(stopDir);

const result = findViteConfigUp(tempDir, stopDir);
expect(result).toBeUndefined();
});

it('should prefer the closest config file', () => {
const subDir = path.join(tempDir, 'packages', 'my-lib');
fs.mkdirSync(subDir, { recursive: true });
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/create/org-resolve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as prompts from '@voidzero-dev/vite-plus-prompts';

import { findWorkspaceRoot, hasViteConfig, resolveViteConfig } from '../resolve-vite-config.ts';
import { findMonorepoRoot } from '../../binding/index.js';
import { hasViteConfig, resolveViteConfig } from '../resolve-vite-config.ts';
import { cancelAndExit } from '../utils/prompts.ts';
import {
CreateConfigSchemaError,
Expand Down Expand Up @@ -204,9 +205,9 @@ export async function resolveOrgManifestForCreate(args: {
* Read the `create` config (`defaultTemplate` + validated `templates`) from
* a workspace's `vite.config.ts` in a single config evaluation.
*
* By default, walks up from `startDir` via `findWorkspaceRoot` (monorepo
* markers only — `pnpm-workspace.yaml`, `workspaces` in `package.json`,
* `lerna.json`) so monorepo invocations from any subdirectory still pick up
* By default, walks up from `startDir` via `findMonorepoRoot` (monorepo
* markers only — `pnpm-workspace.yaml` or `workspaces` in `package.json`)
* so monorepo invocations from any subdirectory still pick up
* the root config. Pass `walkUp: false` to read `startDir` directly when the
* caller already holds the exact workspace root.
*
Expand All @@ -224,7 +225,7 @@ export async function getConfiguredCreate(
options?: { walkUp?: boolean; throwOnReadError?: boolean },
): Promise<{ defaultTemplate?: string; templates: CreateTemplateEntry[] }> {
const projectRoot =
options?.walkUp === false ? startDir : (findWorkspaceRoot(startDir) ?? startDir);
options?.walkUp === false ? startDir : (findMonorepoRoot(startDir) ?? startDir);
if (!hasViteConfig(projectRoot)) {
return { templates: [] };
}
Expand Down
43 changes: 8 additions & 35 deletions packages/cli/src/resolve-vite-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';

import { findMonorepoRoot } from '../binding/index.js';
import { withConfigMetadataResolution } from './define-config.ts';
import { VITE_CONFIG_FILES } from './utils/constants.ts';

Expand All @@ -11,6 +12,11 @@ import { VITE_CONFIG_FILES } from './utils/constants.ts';
export function findViteConfigUp(startDir: string, stopDir: string): string | undefined {
let dir = path.resolve(startDir);
const stop = path.resolve(stopDir);
// Callers may start above `stopDir`, e.g. at the parent of a workspace root.
const relative = path.relative(stop, dir);
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
return undefined;
}

while (true) {
for (const filename of VITE_CONFIG_FILES) {
Expand All @@ -20,7 +26,7 @@ export function findViteConfigUp(startDir: string, stopDir: string): string | un
}
}
const parent = path.dirname(dir);
if (parent === dir || !parent.startsWith(stop)) {
if (dir === stop || parent === dir) {
break;
}
dir = parent;
Expand All @@ -42,39 +48,6 @@ export function hasViteConfig(dir: string): boolean {
return findViteConfig(dir) !== undefined;
}

/**
* Find the workspace root by walking up from `startDir` looking for
* monorepo indicators (pnpm-workspace.yaml, workspaces in package.json, lerna.json).
*/
export function findWorkspaceRoot(startDir: string): string | undefined {
let dir = path.resolve(startDir);
while (true) {
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
return dir;
}
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
if (pkg.workspaces) {
return dir;
}
} catch {
// Skip malformed package.json and continue searching parent directories
}
}
if (fs.existsSync(path.join(dir, 'lerna.json'))) {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return undefined;
}

export interface ResolveViteConfigOptions {
traverseUp?: boolean;
}
Expand All @@ -89,7 +62,7 @@ export async function resolveViteConfig(cwd: string, options?: ResolveViteConfig
// staged/create…), so skip the user's plugin factory while it evaluates.
return withConfigMetadataResolution(async () => {
if (options?.traverseUp && !hasViteConfig(cwd)) {
const workspaceRoot = findWorkspaceRoot(cwd);
const workspaceRoot = findMonorepoRoot(cwd);
if (workspaceRoot) {
const configFile = findViteConfigUp(path.dirname(cwd), workspaceRoot);
if (configFile) {
Expand Down
Loading