From 6ceb9fbf40a17c45ab770acf14618dcd9a2cedce Mon Sep 17 00:00:00 2001 From: MK Date: Sun, 1 Mar 2026 12:34:13 +0800 Subject: [PATCH 1/4] feat(create): improve UX when running `vp create` from monorepo subdirectories When a user runs `vp create` from a subdirectory of a monorepo, the command auto-detects the workspace root and creates packages relative to that root. This can be confusing since the user expects location-awareness. Changes: - Show "Detected monorepo root at " info message when in a subdir - In interactive mode, add a "current directory" option to the parent dir selector when the user's cwd is not under a standard workspace pattern dir, and default to it - In non-interactive mode, show a hint about using --directory - Add snap test covering all edge cases: workspace subdir, workspace parent dir, non-workspace dir, and --directory from a subdir --- .../apps/website/package.json | 4 ++ .../create-from-monorepo-subdir/package.json | 6 ++ .../pnpm-workspace.yaml | 4 ++ .../scripts/helper/package.json | 4 ++ .../create-from-monorepo-subdir/snap.txt | 21 ++++++ .../create-from-monorepo-subdir/steps.json | 29 ++++++++ packages/cli/src/create/bin.ts | 69 ++++++++++++++----- 7 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/apps/website/package.json create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/package.json create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/pnpm-workspace.yaml create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/scripts/helper/package.json create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt create mode 100644 packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/apps/website/package.json b/packages/cli/snap-tests-global/create-from-monorepo-subdir/apps/website/package.json new file mode 100644 index 0000000000..042a63fe8e --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/apps/website/package.json @@ -0,0 +1,4 @@ +{ + "name": "website", + "version": "0.0.0" +} diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/package.json b/packages/cli/snap-tests-global/create-from-monorepo-subdir/package.json new file mode 100644 index 0000000000..3e9925adba --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-monorepo", + "version": "0.0.0", + "private": true, + "packageManager": "pnpm@10.12.1" +} diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/pnpm-workspace.yaml b/packages/cli/snap-tests-global/create-from-monorepo-subdir/pnpm-workspace.yaml new file mode 100644 index 0000000000..7a82ab00f0 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - apps/* + - packages/* + - tools/* diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/scripts/helper/package.json b/packages/cli/snap-tests-global/create-from-monorepo-subdir/scripts/helper/package.json new file mode 100644 index 0000000000..d74302b4c7 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/scripts/helper/package.json @@ -0,0 +1,4 @@ +{ + "name": "helper", + "version": "0.0.0" +} diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt b/packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt new file mode 100644 index 0000000000..79e092d56e --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt @@ -0,0 +1,21 @@ +> cd apps/website && vp create --no-interactive vite:generator # from workspace subdir +> test -f tools/vite-plus-generator/package.json && echo 'Created at tools/vite-plus-generator' || echo 'NOT at tools/' +Created at tools/vite-plus-generator + +> test ! -f apps/website/tools/vite-plus-generator/package.json && echo 'Not in apps/website/' || echo 'BUG: in apps/website/' +Not in apps/website/ + +> cd apps && vp create --no-interactive vite:application # from workspace parent dir +> test -f apps/vite-plus-application/package.json && echo 'Created at apps/vite-plus-application' || echo 'NOT at apps/' +Created at apps/vite-plus-application + +> cd scripts/helper && vp create --no-interactive vite:library # from non-workspace dir +> test -f packages/vite-plus-library/package.json && echo 'Created at packages/vite-plus-library' || echo 'NOT at packages/' +Created at packages/vite-plus-library + +> test ! -f scripts/helper/packages/vite-plus-library/package.json && echo 'Not in scripts/helper/' || echo 'BUG: in scripts/helper/' +Not in scripts/helper/ + +> cd scripts/helper && vp create --no-interactive vite:application --directory apps/custom-app # --directory from non-workspace dir +> test -f apps/custom-app/package.json && echo 'Created at apps/custom-app with --directory' || echo 'NOT at apps/custom-app' +Created at apps/custom-app with --directory diff --git a/packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json b/packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json new file mode 100644 index 0000000000..df59c30332 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json @@ -0,0 +1,29 @@ +{ + "commands": [ + { + "command": "cd apps/website && vp create --no-interactive vite:generator # from workspace subdir", + "ignoreOutput": true + }, + "test -f tools/vite-plus-generator/package.json && echo 'Created at tools/vite-plus-generator' || echo 'NOT at tools/'", + "test ! -f apps/website/tools/vite-plus-generator/package.json && echo 'Not in apps/website/' || echo 'BUG: in apps/website/'", + + { + "command": "cd apps && vp create --no-interactive vite:application # from workspace parent dir", + "ignoreOutput": true + }, + "test -f apps/vite-plus-application/package.json && echo 'Created at apps/vite-plus-application' || echo 'NOT at apps/'", + + { + "command": "cd scripts/helper && vp create --no-interactive vite:library # from non-workspace dir", + "ignoreOutput": true + }, + "test -f packages/vite-plus-library/package.json && echo 'Created at packages/vite-plus-library' || echo 'NOT at packages/'", + "test ! -f scripts/helper/packages/vite-plus-library/package.json && echo 'Not in scripts/helper/' || echo 'BUG: in scripts/helper/'", + + { + "command": "cd scripts/helper && vp create --no-interactive vite:application --directory apps/custom-app # --directory from non-workspace dir", + "ignoreOutput": true + }, + "test -f apps/custom-app/package.json && echo 'Created at apps/custom-app with --directory' || echo 'NOT at apps/custom-app'" + ] +} diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index ed7a3a6976..82461d50b3 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -15,6 +15,7 @@ import { selectAgentTargetPath, writeAgentInstructions, } from '../utils/agent.js'; +import { displayRelative } from '../utils/path.js'; import { defaultInteractive, downloadPackageManager, @@ -184,8 +185,20 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h packageName = formatted.packageName; } - const workspaceInfoOptional = await detectWorkspace(process.cwd()); + const cwd = process.cwd(); + const workspaceInfoOptional = await detectWorkspace(cwd); const isMonorepo = workspaceInfoOptional.isMonorepo; + const cwdRelativeToRoot = + isMonorepo && workspaceInfoOptional.rootDir !== cwd + ? displayRelative(cwd, workspaceInfoOptional.rootDir) + : ''; + const isInSubdirectory = cwdRelativeToRoot !== ''; + const cwdUnderParentDir = isInSubdirectory + ? workspaceInfoOptional.parentDirs.some( + (dir) => cwdRelativeToRoot === dir || cwdRelativeToRoot.startsWith(`${dir}/`), + ) + : true; + const shouldOfferCwdOption = isInSubdirectory && !cwdUnderParentDir; // Interactive mode: prompt for template if not provided let selectedTemplateName = templateName as string; @@ -287,27 +300,44 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h cancelAndExit('Cannot create a monorepo inside an existing monorepo', 1); } + if (isInSubdirectory) { + prompts.log.info(`Detected monorepo root at ${accent(workspaceInfoOptional.rootDir)}`); + } + if (isMonorepo && options.interactive && !targetDir) { let parentDir: string | undefined; - if (workspaceInfoOptional.parentDirs.length > 0) { - const defaultParentDir = - inferParentDir(selectedTemplateName, workspaceInfoOptional) ?? - workspaceInfoOptional.parentDirs[0]; + const hasParentDirs = workspaceInfoOptional.parentDirs.length > 0; + + if (hasParentDirs || isInSubdirectory) { + const dirOptions: { label: string; value: string; hint: string }[] = + workspaceInfoOptional.parentDirs.map((dir) => ({ + label: `${dir}/`, + value: dir, + hint: '', + })); + + if (shouldOfferCwdOption) { + dirOptions.push({ + label: `${cwdRelativeToRoot}/ (current directory)`, + value: cwdRelativeToRoot, + hint: '', + }); + } + + dirOptions.push({ + label: 'other', + value: 'other', + hint: 'Enter a custom target directory', + }); + + const defaultParentDir = shouldOfferCwdOption + ? cwdRelativeToRoot + : (inferParentDir(selectedTemplateName, workspaceInfoOptional) ?? + workspaceInfoOptional.parentDirs[0]); + const selected = await prompts.select({ message: 'Where should the new package be added to the monorepo:', - options: workspaceInfoOptional.parentDirs - .map((dir) => ({ - label: `${dir}/`, - value: dir, - hint: ``, - })) - .concat([ - { - label: 'other', - value: 'other', - hint: 'Enter a custom target directory', - }, - ]), + options: dirOptions, initialValue: defaultParentDir, }); @@ -339,6 +369,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h selectedParentDir = parentDir; } if (isMonorepo && !options.interactive && !targetDir) { + if (isInSubdirectory) { + prompts.log.info(`Use ${accent('--directory')} to specify a different target location.`); + } const inferredParentDir = inferParentDir(selectedTemplateName, workspaceInfoOptional) ?? workspaceInfoOptional.parentDirs[0]; From 023c3e67da4747741496f24176eb4e1b576d5eef Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 2 Mar 2026 10:02:26 +0800 Subject: [PATCH 2/4] fix(create): use cwd as rootDir for non-monorepo `vp create` When running `vp create` from a subdirectory that has a plain package.json (without workspaces) somewhere up the tree, detectWorkspace would walk up and set rootDir to that parent directory. This caused projects to be created at the parent root instead of the user's current directory. Reset rootDir to cwd when isMonorepo is false, since the parent root is only meaningful for monorepo workspace resolution. --- .../create-from-nonworkspace-subdir/package.json | 4 ++++ .../create-from-nonworkspace-subdir/scripts/.keep | 0 .../create-from-nonworkspace-subdir/snap.txt | 6 ++++++ .../create-from-nonworkspace-subdir/steps.json | 10 ++++++++++ packages/cli/src/create/bin.ts | 7 +++++++ 5 files changed, 27 insertions(+) create mode 100644 packages/cli/snap-tests-global/create-from-nonworkspace-subdir/package.json create mode 100644 packages/cli/snap-tests-global/create-from-nonworkspace-subdir/scripts/.keep create mode 100644 packages/cli/snap-tests-global/create-from-nonworkspace-subdir/snap.txt create mode 100644 packages/cli/snap-tests-global/create-from-nonworkspace-subdir/steps.json diff --git a/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/package.json b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/package.json new file mode 100644 index 0000000000..8b8be750e5 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/package.json @@ -0,0 +1,4 @@ +{ + "name": "parent-project", + "version": "0.0.0" +} diff --git a/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/scripts/.keep b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/scripts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/snap.txt b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/snap.txt new file mode 100644 index 0000000000..2a1b2ea212 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/snap.txt @@ -0,0 +1,6 @@ +> cd scripts && vp create --no-interactive vite:application # from non-monorepo subdir +> test -f scripts/vite-plus-application/package.json && echo 'Created at scripts/vite-plus-application' || echo 'NOT at scripts/' +Created at scripts/vite-plus-application + +> test ! -f vite-plus-application/package.json && echo 'Not at parent root' || echo 'BUG: created at parent root' +Not at parent root diff --git a/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/steps.json b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/steps.json new file mode 100644 index 0000000000..bbbd6abbd6 --- /dev/null +++ b/packages/cli/snap-tests-global/create-from-nonworkspace-subdir/steps.json @@ -0,0 +1,10 @@ +{ + "commands": [ + { + "command": "cd scripts && vp create --no-interactive vite:application # from non-monorepo subdir", + "ignoreOutput": true + }, + "test -f scripts/vite-plus-application/package.json && echo 'Created at scripts/vite-plus-application' || echo 'NOT at scripts/'", + "test ! -f vite-plus-application/package.json && echo 'Not at parent root' || echo 'BUG: created at parent root'" + ] +} diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 82461d50b3..a0e430ac12 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -188,6 +188,13 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h const cwd = process.cwd(); const workspaceInfoOptional = await detectWorkspace(cwd); const isMonorepo = workspaceInfoOptional.isMonorepo; + + // For non-monorepo, always use cwd as rootDir. + // detectWorkspace walks up to find the nearest package.json, but for `vp create` + // in standalone mode, the project should be created relative to where the user is. + if (!isMonorepo) { + workspaceInfoOptional.rootDir = cwd; + } const cwdRelativeToRoot = isMonorepo && workspaceInfoOptional.rootDir !== cwd ? displayRelative(cwd, workspaceInfoOptional.rootDir) From 70b9c1b2b88d962270436385b36c556d6c7f6fd1 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 2 Mar 2026 12:53:34 +0800 Subject: [PATCH 3/4] fix(create): accept default package name when pressing Enter without typing When the user presses Enter on the "Package name:" prompt without typing, `@clack/core` passes `undefined` as the value to validate. The old condition `value != null && value.length === 0` skipped `undefined`, causing validation to fail with "Invalid package name". Changed to `value == null || value.length === 0` so both `undefined` and empty string are treated as "use the default". --- packages/cli/src/create/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/create/prompts.ts b/packages/cli/src/create/prompts.ts index 6d170c9c6c..f270a7fded 100644 --- a/packages/cli/src/create/prompts.ts +++ b/packages/cli/src/create/prompts.ts @@ -20,7 +20,7 @@ export async function promptPackageNameAndTargetDir( placeholder: defaultPackageName, defaultValue: defaultPackageName, validate: (value) => { - if (value != null && value.length === 0) { + if (value == null || value.length === 0) { return; } From a97684769a85c8c430f80c26bf5229e11a100237 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 2 Mar 2026 22:44:09 +0800 Subject: [PATCH 4/4] fix(create): rename "other" to "other directory" in monorepo parent dir prompt --- packages/cli/src/create/bin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index a0e430ac12..6a1cd8c094 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -332,7 +332,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } dirOptions.push({ - label: 'other', + label: 'other directory', value: 'other', hint: 'Enter a custom target directory', });