From b5085c1e8a966c2aa177b3cc6c37bdcc0eff2cf6 Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 8 Aug 2026 05:30:09 -0500 Subject: [PATCH] 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 --- .../create/CreateProjectModal.test.tsx | 41 +++++++++++++++++++ .../components/create/CreateProjectModal.tsx | 27 +++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/create/CreateProjectModal.test.tsx diff --git a/src/renderer/src/components/create/CreateProjectModal.test.tsx b/src/renderer/src/components/create/CreateProjectModal.test.tsx new file mode 100644 index 0000000..63168fb --- /dev/null +++ b/src/renderer/src/components/create/CreateProjectModal.test.tsx @@ -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) + }) +}) diff --git a/src/renderer/src/components/create/CreateProjectModal.tsx b/src/renderer/src/components/create/CreateProjectModal.tsx index abdf478..59e58cf 100644 --- a/src/renderer/src/components/create/CreateProjectModal.tsx +++ b/src/renderer/src/components/create/CreateProjectModal.tsx @@ -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(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 { @@ -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 && ( +

+ Use only letters, numbers, and hyphens — no spaces (e.g. " + {slugifyProjectName(trimmedName) || 'my-project'}"). +

+ )}