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 index 79e092d56e..187bec6010 100644 --- a/packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/snap.txt @@ -19,3 +19,19 @@ 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 + +> mkdir -p apps/dot-test && cd apps/dot-test && vp create --no-interactive vite:application --directory . # --directory . from monorepo subdir +> test -f apps/dot-test/package.json && echo 'Created at apps/dot-test with --directory .' || echo 'NOT at apps/dot-test' +Created at apps/dot-test with --directory . + +> vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . from monorepo root should fail +Cannot scaffold into the monorepo root directory. Use --directory to specify a target directory + + +> mkdir -p apps/website/src && cd apps/website/src && vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . inside existing package should fail +Cannot scaffold inside existing package "website" (apps/website). Use --directory to specify a different location + + +> cd apps/website && vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . at existing package root should fail +Cannot scaffold inside existing package "website" (apps/website). Use --directory to specify a different location + 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 index 692f8d5e48..72df0f7725 100644 --- a/packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json +++ b/packages/cli/snap-tests-global/create-from-monorepo-subdir/steps.json @@ -25,6 +25,18 @@ "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'" + "test -f apps/custom-app/package.json && echo 'Created at apps/custom-app with --directory' || echo 'NOT at apps/custom-app'", + + { + "command": "mkdir -p apps/dot-test && cd apps/dot-test && vp create --no-interactive vite:application --directory . # --directory . from monorepo subdir", + "ignoreOutput": true + }, + "test -f apps/dot-test/package.json && echo 'Created at apps/dot-test with --directory .' || echo 'NOT at apps/dot-test'", + + "vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . from monorepo root should fail", + + "mkdir -p apps/website/src && cd apps/website/src && vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . inside existing package should fail", + + "cd apps/website && vp create --no-interactive vite:application --directory . 2>&1 || true # --directory . at existing package root should fail" ] } diff --git a/packages/cli/snap-tests-global/new-create-vite-directory-dot/snap.txt b/packages/cli/snap-tests-global/new-create-vite-directory-dot/snap.txt new file mode 100644 index 0000000000..b07de24e2c --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-directory-dot/snap.txt @@ -0,0 +1,3 @@ +> mkdir -p my-app && cd my-app && vp create vite:application --no-interactive --directory . # create vite app in current directory +> test -f my-app/package.json && echo 'Created at my-app with --directory .' || echo 'NOT at my-app' +Created at my-app with --directory . diff --git a/packages/cli/snap-tests-global/new-create-vite-directory-dot/steps.json b/packages/cli/snap-tests-global/new-create-vite-directory-dot/steps.json new file mode 100644 index 0000000000..10ad8fc718 --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-directory-dot/steps.json @@ -0,0 +1,9 @@ +{ + "commands": [ + { + "command": "mkdir -p my-app && cd my-app && vp create vite:application --no-interactive --directory . # create vite app in current directory", + "ignoreOutput": true + }, + "test -f my-app/package.json && echo 'Created at my-app with --directory .' || echo 'NOT at my-app'" + ] +} diff --git a/packages/cli/src/create/__tests__/__snapshots__/utils.spec.ts.snap b/packages/cli/src/create/__tests__/__snapshots__/utils.spec.ts.snap index 4846bbac07..a8bbed12ce 100644 --- a/packages/cli/src/create/__tests__/__snapshots__/utils.spec.ts.snap +++ b/packages/cli/src/create/__tests__/__snapshots__/utils.spec.ts.snap @@ -1,14 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`formatTargetDir > should format target dir with invalid input 1`] = ` -{ - "directory": "", - "error": "Parsed package name "." is invalid: name cannot start with a period", - "packageName": "", -} -`; - -exports[`formatTargetDir > should format target dir with invalid input 2`] = ` { "directory": "", "error": "Absolute path is not allowed", @@ -16,7 +8,7 @@ exports[`formatTargetDir > should format target dir with invalid input 2`] = ` } `; -exports[`formatTargetDir > should format target dir with invalid input 3`] = ` +exports[`formatTargetDir > should format target dir with invalid input 2`] = ` { "directory": "", "error": "Parsed package name "@scope" is invalid: name can only contain URL-friendly characters", @@ -24,7 +16,7 @@ exports[`formatTargetDir > should format target dir with invalid input 3`] = ` } `; -exports[`formatTargetDir > should format target dir with invalid input 4`] = ` +exports[`formatTargetDir > should format target dir with invalid input 3`] = ` { "directory": "", "error": "Relative path contains ".." which is not allowed", diff --git a/packages/cli/src/create/__tests__/utils.spec.ts b/packages/cli/src/create/__tests__/utils.spec.ts index 12d75e0e5b..fd01ce4ed8 100644 --- a/packages/cli/src/create/__tests__/utils.spec.ts +++ b/packages/cli/src/create/__tests__/utils.spec.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { formatTargetDir, getProjectDirFromPackageName } from '../utils.js'; +import { + deriveDefaultPackageName, + formatTargetDir, + getProjectDirFromPackageName, +} from '../utils.js'; describe('getProjectDirFromPackageName', () => { it('should get project dir from package name', () => { @@ -10,8 +14,21 @@ describe('getProjectDirFromPackageName', () => { }); describe('formatTargetDir', () => { + it('should format "." as current directory with empty package name', () => { + expect(formatTargetDir('.')).toEqual({ + directory: '.', + packageName: '', + }); + }); + + it('should format "./" as current directory with empty package name', () => { + expect(formatTargetDir('./')).toEqual({ + directory: '.', + packageName: '', + }); + }); + it('should format target dir with invalid input', () => { - expect(formatTargetDir('.')).matchSnapshot(); expect(formatTargetDir('/foo/bar')).matchSnapshot(); expect(formatTargetDir('@scope/')).matchSnapshot(); expect(formatTargetDir('../../foo/bar')).matchSnapshot(); @@ -45,3 +62,28 @@ describe('formatTargetDir', () => { expect(formatTargetDir('my-package@1.0.0').error).matchSnapshot(); }); }); + +describe('deriveDefaultPackageName', () => { + it('should derive package name from directory basename', () => { + expect(deriveDefaultPackageName('/home/user/my-app', undefined, 'fallback')).toBe('my-app'); + }); + + it('should derive scoped package name when scope is provided', () => { + expect(deriveDefaultPackageName('/home/user/my-app', '@my-scope', 'fallback')).toBe( + '@my-scope/my-app', + ); + }); + + it('should fallback to random name when directory name is invalid', () => { + const result = deriveDefaultPackageName('/home/user/.hidden', undefined, 'vite-plus-app'); + // directory name starts with '.', so a random name is generated instead + expect(result).not.toBe('.hidden'); + expect(result.length).toBeGreaterThan(0); + }); + + it('should fallback when directory is filesystem root', () => { + const result = deriveDefaultPackageName('/', undefined, 'vite-plus-app'); + // basename of '/' is empty, so a random name is generated + expect(result.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index c16b0ffeaf..63b0f2ce4d 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -53,7 +53,7 @@ import { } from './templates/index.js'; import { InitialMonorepoAppDir } from './templates/monorepo.js'; import { BuiltinTemplate, TemplateType } from './templates/types.js'; -import { formatTargetDir } from './utils.js'; +import { deriveDefaultPackageName, formatTargetDir } from './utils.js'; const helpMessage = renderCliDoc({ usage: 'vp create [TEMPLATE] [OPTIONS] [-- TEMPLATE_OPTIONS]', @@ -570,8 +570,42 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } } - if (isBuiltinTemplate && !targetDir) { - if (selectedTemplateName === BuiltinTemplate.monorepo) { + if (isBuiltinTemplate && (!targetDir || targetDir === '.')) { + if (targetDir === '.') { + // Current directory: auto-derive package name from cwd, no prompt + const fallbackName = + selectedTemplateName === BuiltinTemplate.monorepo + ? 'vite-plus-monorepo' + : `vite-plus-${selectedTemplateName.split(':')[1]}`; + packageName = deriveDefaultPackageName( + cwd, + workspaceInfoOptional.monorepoScope, + fallbackName, + ); + if (isMonorepo) { + if (!cwdRelativeToRoot) { + // At monorepo root: scaffolding here would overwrite the entire workspace + cancelAndExit( + 'Cannot scaffold into the monorepo root directory. Use --directory to specify a target directory', + 1, + ); + } + // Check if cwd is inside an existing workspace package + const enclosingPackage = workspaceInfoOptional.packages.find( + (pkg) => cwdRelativeToRoot === pkg.path || cwdRelativeToRoot.startsWith(`${pkg.path}/`), + ); + if (enclosingPackage) { + cancelAndExit( + `Cannot scaffold inside existing package "${enclosingPackage.name}" (${enclosingPackage.path}). Use --directory to specify a different location`, + 1, + ); + } + // Resolve '.' to the path relative to rootDir + // so that scaffolding happens in cwd, not at the workspace root + targetDir = cwdRelativeToRoot; + } + prompts.log.info(`Using package name: ${accent(packageName)}`); + } else if (selectedTemplateName === BuiltinTemplate.monorepo) { const selected = await promptPackageNameAndTargetDir( getRandomProjectName({ fallbackName: 'vite-plus-monorepo' }), options.interactive, @@ -788,7 +822,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h : selected.targetDir; } pauseCreateProgress(); - await checkProjectDirExists(targetDir, options.interactive); + await checkProjectDirExists(path.join(workspaceInfo.rootDir, targetDir), options.interactive); resumeCreateProgress(); updateCreateProgress('Generating project'); result = await executeBuiltinTemplate( diff --git a/packages/cli/src/create/prompts.ts b/packages/cli/src/create/prompts.ts index 4b003a51f9..aecb609e1d 100644 --- a/packages/cli/src/create/prompts.ts +++ b/packages/cli/src/create/prompts.ts @@ -24,7 +24,6 @@ export async function promptPackageNameAndTargetDir( if (value == null || value.length === 0) { return; } - const result = value ? validateNpmPackageName(value) : null; if (result?.validForNewPackages) { return; diff --git a/packages/cli/src/create/utils.ts b/packages/cli/src/create/utils.ts index cf19781f83..38f5098faa 100644 --- a/packages/cli/src/create/utils.ts +++ b/packages/cli/src/create/utils.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import validateNpmPackageName from 'validate-npm-package-name'; import { editJsonFile } from '../utils/json.js'; +import { getRandomProjectName } from './random-name.js'; // Helper functions for file operations export function copy(src: string, dest: string) { @@ -30,12 +31,12 @@ export function copyDir(srcDir: string, destDir: string) { * Examples: * ``` * # invalid target directories - * ./ -> { directory: '', packageName: '', error: 'Invalid target directory' } * /foo/bar -> { directory: '', packageName: '', error: 'Absolute path is not allowed' } * @scope/ -> { directory: '', packageName: '', error: 'Invalid target directory' } * ../../foo/bar -> { directory: '', packageName: '', error: 'Invalid target directory' } * * # valid target directories + * . -> { directory: '.', packageName: '' } * ./my-package -> { directory: './my-package', packageName: 'my-package' } * ./foo/bar-package -> { directory: './foo/bar-package', packageName: 'bar-package' } * ./foo/bar-package/ -> { directory: './foo/bar-package', packageName: 'bar-package' } @@ -52,6 +53,12 @@ export function formatTargetDir(input: string): { error?: string; } { let targetDir = path.normalize(input.trim()); + + // "." or "./" means current directory — valid directory, but no package name derivable + if (targetDir === '.' || targetDir === `.${path.sep}`) { + return { directory: '.', packageName: '' }; + } + const parsed = path.parse(targetDir); if (parsed.root || path.isAbsolute(targetDir)) { return { @@ -120,3 +127,15 @@ export function formatDisplayTargetDir(targetDir: string) { } return `./${normalized}`; } + +export function deriveDefaultPackageName( + cwd: string, + scope: string | undefined, + fallbackName: string, +): string { + const dirName = path.basename(cwd); + const candidate = scope ? `${scope}/${dirName}` : dirName; + return validateNpmPackageName(candidate).validForNewPackages + ? candidate + : getRandomProjectName({ scope, fallbackName }); +}