Skip to content

Commit bdba45d

Browse files
committed
refactor(create): fold findWorkspaceRoot to a single pass + small cleanups
Post-simplify cleanup: - `findWorkspaceRoot` now uses a single walk with a deferred `.git` match. The two-pass version walked the entire ancestry twice in the no-marker case; now we record the first `.git` seen and return it only after the walk exhausts without finding a monorepo marker. Same "markers win globally" semantics, half the I/O. - Export `hasViteConfig` and use it in `getConfiguredDefaultTemplate` instead of the degenerate `findViteConfigUp(x, x)` (start == stop meant it only checked the single directory anyway). - Drop the dead `?? 'template'` fallback in `bin.ts` — `bundledEntryName` is always set whenever `isBundledTemplate` is true (both come from the same `resolved.kind === 'bundled'` branch). - `fetchNpmResource` init type is now `Omit<RequestInit, 'signal'>` so callers can't accidentally pass a `signal` that the `AbortSignal.timeout(timeoutMs)` inside would silently override. - Tighten `expandNpmrcValue` JSDoc to list the four `.npmrc` features we do NOT handle (backslash escapes, `${VAR-default}`, inline `;` comments, `key[]=` lists).
1 parent 24425dc commit bdba45d

4 files changed

Lines changed: 22 additions & 34 deletions

File tree

packages/cli/src/create/bin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
633633
}
634634

635635
const directScaffoldFallbackName = isBundledTemplate
636-
? `vite-plus-${bundledEntryName ?? 'template'}`
636+
? `vite-plus-${bundledEntryName}`
637637
: selectedTemplateName === BuiltinTemplate.monorepo
638638
? 'vite-plus-monorepo'
639639
: `vite-plus-${selectedTemplateName.split(':')[1]}`;

packages/cli/src/create/org-resolve.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as prompts from '@voidzero-dev/vite-plus-prompts';
22

3-
import { findViteConfigUp, findWorkspaceRoot, resolveViteConfig } from '../resolve-vite-config.ts';
3+
import { findWorkspaceRoot, hasViteConfig, resolveViteConfig } from '../resolve-vite-config.ts';
44
import {
55
filterManifestForContext,
66
isRelativePath,
@@ -202,7 +202,7 @@ export async function resolveOrgManifestForCreate(args: {
202202
*/
203203
export async function getConfiguredDefaultTemplate(startDir: string): Promise<string | undefined> {
204204
const projectRoot = findWorkspaceRoot(startDir) ?? startDir;
205-
if (!findViteConfigUp(projectRoot, projectRoot)) {
205+
if (!hasViteConfig(projectRoot)) {
206206
return undefined;
207207
}
208208
try {

packages/cli/src/resolve-vite-config.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,23 @@ export function findViteConfigUp(startDir: string, stopDir: string): string | un
3434
return undefined;
3535
}
3636

37-
function hasViteConfig(dir: string): boolean {
37+
export function hasViteConfig(dir: string): boolean {
3838
return VITE_CONFIG_FILES.some((f) => fs.existsSync(path.join(dir, f)));
3939
}
4040

4141
/**
4242
* Find the workspace / repo root by walking up from `startDir`.
4343
*
44-
* Primary pass: stop at the first `pnpm-workspace.yaml`, `workspaces`
45-
* in `package.json`, or `lerna.json` — the authoritative monorepo
46-
* markers.
47-
*
48-
* Fallback pass: if no marker is found anywhere in the ancestry, walk
49-
* again looking for `.git` so standalone git repos still resolve to
50-
* their repo boundary. `.git` is deliberately NOT checked in the first
51-
* pass — a subproject with its own `.git` (git submodule, nested clone)
52-
* must not preempt a monorepo marker higher up the tree.
44+
* Monorepo markers (`pnpm-workspace.yaml`, `workspaces` in `package.json`,
45+
* `lerna.json`) win globally — they always take precedence over any
46+
* `.git` seen along the way, so a subproject with its own `.git` (git
47+
* submodule, nested clone) can't preempt a marker higher up the tree.
48+
* `.git` is only returned when the walk finishes without finding any
49+
* monorepo marker.
5350
*/
5451
export function findWorkspaceRoot(startDir: string): string | undefined {
55-
const startResolved = path.resolve(startDir);
56-
let dir = startResolved;
52+
let dir = path.resolve(startDir);
53+
let gitFallback: string | undefined;
5754
while (true) {
5855
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
5956
return dir;
@@ -72,26 +69,16 @@ export function findWorkspaceRoot(startDir: string): string | undefined {
7269
if (fs.existsSync(path.join(dir, 'lerna.json'))) {
7370
return dir;
7471
}
75-
const parent = path.dirname(dir);
76-
if (parent === dir) {
77-
break;
78-
}
79-
dir = parent;
80-
}
81-
// Fallback: no monorepo marker found in the ancestry. Walk again
82-
// looking for a `.git` directory as the standalone repo boundary.
83-
dir = startResolved;
84-
while (true) {
85-
if (fs.existsSync(path.join(dir, '.git'))) {
86-
return dir;
72+
if (gitFallback === undefined && fs.existsSync(path.join(dir, '.git'))) {
73+
gitFallback = dir;
8774
}
8875
const parent = path.dirname(dir);
8976
if (parent === dir) {
9077
break;
9178
}
9279
dir = parent;
9380
}
94-
return undefined;
81+
return gitFallback;
9582
}
9683

9784
export interface ResolveViteConfigOptions {

packages/cli/src/utils/npm-config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import path from 'node:path';
55
type NpmConfig = Map<string, string>;
66

77
function expandNpmrcValue(raw: string): string {
8-
// Strip surrounding quotes and expand `${VAR}` references.
9-
// Narrow subset of npm's `.npmrc` behavior — covers only the keys we
10-
// actually read (registry / @scope:registry / :_authToken / :_auth /
11-
// :username / :_password). Escape sequences and nested expansion are
12-
// intentionally ignored.
8+
// Strip surrounding quotes and expand `${VAR}` references. Covers only
9+
// the value shapes used by the keys we actually read (registry /
10+
// @scope:registry / :_authToken / :_auth / :username / :_password).
11+
// Intentionally NOT handled: `\$` backslash escapes, `${VAR-default}`
12+
// fallbacks, inline `;` comments after a value, and `key[]=` list
13+
// syntax. Extend when a caller needs any of those.
1314
let value = raw.trim();
1415
if (
1516
(value.startsWith('"') && value.endsWith('"')) ||
@@ -167,7 +168,7 @@ export function getNpmAuthHeader(registryOrUrl: string): string | undefined {
167168
*/
168169
export async function fetchNpmResource(
169170
url: string,
170-
init: RequestInit & { timeoutMs: number },
171+
init: Omit<RequestInit, 'signal'> & { timeoutMs: number },
171172
): Promise<Response> {
172173
const { timeoutMs, headers: callerHeaders, ...rest } = init;
173174
const first = await fetch(url, {

0 commit comments

Comments
 (0)