diff --git a/packages/create-void/src/args.ts b/packages/create-void/src/args.ts index 07e4f404a..e1e8b50be 100644 --- a/packages/create-void/src/args.ts +++ b/packages/create-void/src/args.ts @@ -7,10 +7,24 @@ export type CreateArgs = { // `--framework react` or `--deploy=none` to `void init`. const INIT_ARG_PATTERN = /^[\w=-]+$/; +export function validateProjectName(value: string | undefined): string | undefined { + if (value && !value.trim()) { + return 'Project name cannot be blank'; + } + if (value !== undefined && value !== value.toLowerCase()) { + return 'Project name must be lowercase'; + } + return undefined; +} + /** Split `create-void [name] [void init options]` into the name and the options. */ export function parseCreateArgs(argv: ReadonlyArray): CreateArgs { const [first, ...rest] = argv; const appName = first !== undefined && !first.startsWith('-') ? first : undefined; + const nameError = validateProjectName(appName); + if (nameError) { + throw new Error(`create-void: ${nameError}`); + } const initArgs = appName === undefined ? [...argv] : rest; for (const arg of initArgs) { if (!INIT_ARG_PATTERN.test(arg)) { diff --git a/packages/create-void/src/index.ts b/packages/create-void/src/index.ts index 8e49a8013..4aaefae26 100644 --- a/packages/create-void/src/index.ts +++ b/packages/create-void/src/index.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from import * as p from '@voidzero-dev/vite-plus-prompts'; import { resolve } from 'pathslash'; import pc from 'picocolors'; -import { parseCreateArgs } from './args.ts'; +import { parseCreateArgs, validateProjectName } from './args.ts'; import { detectPM, pmExec, pmInstall, run, writePnpmBuildPolicy } from './pm.ts'; const pm = detectPM(); @@ -25,11 +25,7 @@ async function main() { message: 'Project name:', defaultValue: 'my-app', placeholder: 'my-app', - validate(value) { - if (value && !value.trim()) { - return 'Project name cannot be blank'; - } - }, + validate: validateProjectName, }); if (p.isCancel(name) || typeof name !== 'string') { p.cancel('Cancelled.'); diff --git a/packages/create-void/test/unit/args.test.ts b/packages/create-void/test/unit/args.test.ts index 86f799c59..9795a39ff 100644 --- a/packages/create-void/test/unit/args.test.ts +++ b/packages/create-void/test/unit/args.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vite-plus/test'; -import { parseCreateArgs } from '../../src/args.ts'; +import { parseCreateArgs, validateProjectName } from '../../src/args.ts'; describe('parseCreateArgs', () => { it('forwards options after the project name to void init', () => { @@ -17,9 +17,29 @@ describe('parseCreateArgs', () => { expect(parseCreateArgs([])).toEqual({ appName: undefined, initArgs: [] }); }); + it.each(['MyApp', 'my-App', 'MY-APP'])('rejects an uppercase project name: %s', (name) => { + expect(() => parseCreateArgs([name])).toThrow('create-void: Project name must be lowercase'); + }); + it('rejects arguments that are not plain flags or values', () => { expect(() => parseCreateArgs(['my-app', '--framework', 'react&calc'])).toThrow( "create-void: Unsupported argument 'react&calc'.", ); }); }); + +describe('validateProjectName', () => { + it.each(['MyApp', 'my-App', 'MY-APP'])('rejects uppercase names: %s', (name) => { + expect(validateProjectName(name)).toBe('Project name must be lowercase'); + }); + + it.each(['my-app', 'app123', 'nested/my-app', '.'])('accepts lowercase names: %s', (name) => { + expect(validateProjectName(name)).toBeUndefined(); + }); + + it('allows the prompt default and rejects whitespace-only names', () => { + expect(validateProjectName(undefined)).toBeUndefined(); + expect(validateProjectName('')).toBeUndefined(); + expect(validateProjectName(' ')).toBe('Project name cannot be blank'); + }); +});