Validate and slugify project names in the create wizard

DDEV project names must be valid hostname labels. The wizard
auto-filled the name from the picked folder's basename verbatim, so a
folder like "Aurora Admin" produced a name with a space that DDEV's
`ddev config` rejected with an opaque CLI error the UI never
surfaced. Slugify the auto-filled name and block manual entry of
invalid names before they ever reach the ddev CLI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
reaper
2026-08-08 05:30:09 -05:00
co-authored by Claude Sonnet 5
parent 86be76132d
commit b5085c1e8a
2 changed files with 66 additions and 2 deletions
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { isValidProjectName, slugifyProjectName } from './CreateProjectModal'
describe('isValidProjectName', () => {
it('accepts hostname-safe names', () => {
expect(isValidProjectName('my-project')).toBe(true)
expect(isValidProjectName('project1')).toBe(true)
})
it('rejects names with spaces', () => {
expect(isValidProjectName('Aurora Admin')).toBe(false)
})
it('rejects names starting or ending with a hyphen', () => {
expect(isValidProjectName('-project')).toBe(false)
expect(isValidProjectName('project-')).toBe(false)
})
it('rejects empty names', () => {
expect(isValidProjectName('')).toBe(false)
})
})
describe('slugifyProjectName', () => {
it('lowercases and replaces spaces with hyphens', () => {
expect(slugifyProjectName('Aurora Admin')).toBe('aurora-admin')
})
it('collapses runs of non-alphanumeric characters', () => {
expect(slugifyProjectName('My Cool!! Project')).toBe('my-cool-project')
})
it('trims leading and trailing hyphens', () => {
expect(slugifyProjectName(' -Weird Name- ')).toBe('weird-name')
})
it('produces a name that passes validation', () => {
const slug = slugifyProjectName('Aurora Admin')
expect(isValidProjectName(slug)).toBe(true)
})
})
@@ -23,6 +23,21 @@ import docksideIcon from '../../assets/dockside-icon.png'
type Step = 'site' | 'setup'
// DDEV project names must be valid hostname labels: alphanumeric and
// hyphens only, can't start/end with a hyphen.
const PROJECT_NAME_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/
export function isValidProjectName(name: string): boolean {
return PROJECT_NAME_PATTERN.test(name)
}
export function slugifyProjectName(raw: string): string {
return raw
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
const fieldClass =
'w-full rounded-lg border border-neutral-300 bg-white/80 px-3 py-2 text-sm shadow-sm transition placeholder:text-neutral-400 focus:border-cyan-400 dark:border-white/10 dark:bg-neutral-950/70 dark:placeholder:text-neutral-600'
@@ -56,7 +71,9 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
const selectProject = useAppStore((s) => s.selectProject)
const setupRef = useRef<TypeSetupHandle>(null)
const canContinue = directory !== null && projectName.trim().length > 0
const trimmedName = projectName.trim()
const nameValid = trimmedName.length > 0 && isValidProjectName(trimmedName)
const canContinue = directory !== null && nameValid
const canSubmit = canContinue && setupValid && !isSubmitting
async function handlePickDirectory(): Promise<void> {
@@ -65,7 +82,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
setDirectory(picked)
if (!projectName) {
const name = picked.split('/').filter(Boolean).pop() ?? ''
setProjectName(name)
setProjectName(slugifyProjectName(name))
}
}
@@ -198,6 +215,12 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
placeholder="my-project"
className={fieldClass}
/>
{trimmedName.length > 0 && !nameValid && (
<p className="mt-1.5 text-xs text-red-600 dark:text-red-400">
Use only letters, numbers, and hyphens no spaces (e.g. &quot;
{slugifyProjectName(trimmedName) || 'my-project'}&quot;).
</p>
)}
</div>
<div>