Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/src/cli/commands/app/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default class Init extends AppLinkedCommand {
async function getAppName(directory: string): Promise<string> {
for (let i = 0; i < 3; i++) {
// eslint-disable-next-line no-await-in-loop
const name = await generateRandomNameForSubdirectory({suffix: 'app', directory})
const name = await generateRandomNameForSubdirectory({suffix: 'app', directory, titleCase: true})
if (isValidName(name)) return name
}
return ''
Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/cli/prompts/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,34 @@ describe('appName', () => {
expect(renderTextPrompt).toHaveBeenCalledWith({
message: 'App name',
defaultValue: 'suggested-name',
preview: expect.any(Function),
validate: expect.any(Function),
})
})

test('preview shows hyphenated folder name for Title Case input', async () => {
// Given
vi.mocked(renderTextPrompt).mockResolvedValue('My Cool App')

// When
await appNamePrompt('Default Name')

// Then
const call = vi.mocked(renderTextPrompt).mock.calls[0]![0] as {preview: (value: string) => unknown}
expect(call.preview('My Cool App')).toEqual(['Folder name:', {userInput: 'my-cool-app'}])
})

test('preview falls back to defaultValue when input is empty', async () => {
// Given
vi.mocked(renderTextPrompt).mockResolvedValue('Default Name')

// When
await appNamePrompt('Default Name')

// Then
const call = vi.mocked(renderTextPrompt).mock.calls[0]![0] as {preview: (value: string) => unknown}
expect(call.preview('')).toEqual(['Folder name:', {userInput: 'default-name'}])
})
})

describe('reloadStoreList', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/cli/prompts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
renderTextPrompt,
} from '@shopify/cli-kit/node/ui'
import {outputCompleted} from '@shopify/cli-kit/node/output'
import {hyphenate} from '@shopify/cli-kit/common/string'

export async function selectOrganizationPrompt(organizations: Organization[]): Promise<Organization> {
if (organizations.length === 1) {
Expand Down Expand Up @@ -143,6 +144,10 @@ export async function appNamePrompt(currentName: string): Promise<string> {
return renderTextPrompt({
message: 'App name',
defaultValue: currentName,
preview: (value) => {
const folderName = hyphenate(value || currentName)
return ['Folder name:', {userInput: folderName}]
},
validate: (value) => {
if (value.length === 0) {
return "App name can't be empty"
Expand Down
22 changes: 22 additions & 0 deletions packages/cli-kit/src/public/common/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
formatDate,
formatLocalDate,
getRandomName,
getRandomTitleCaseName,
joinWithAnd,
linesToColumns,
normalizeDelimitedString,
Expand All @@ -21,6 +22,27 @@ describe('getRandomName', () => {
})
})

describe('getRandomTitleCaseName', () => {
test('generates a Title Case name with spaces', () => {
const got = getRandomTitleCaseName()
expect(got.length).not.toBe(0)
// Should not contain hyphens, should have spaces and capitalized words
expect(got).not.toContain('-')
expect(got).toContain(' ')
// Each word should start with an uppercase letter
got.split(' ').forEach((word) => {
expect(word[0]).toBe(word[0]!.toUpperCase())
})
})

test('works with creative family', () => {
const got = getRandomTitleCaseName('creative')
expect(got.length).not.toBe(0)
expect(got).not.toContain('-')
expect(got).toContain(' ')
})
})

describe('tryParseInt', () => {
test('converts a string to an int', () => {
expect(tryParseInt(' 999 ')).toEqual(999)
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/common/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ export function getRandomName(family: RandomNameFamily = 'business'): string {
return `${takeRandomFromArray(mapping[family].adjectives)}-${takeRandomFromArray(mapping[family].nouns)}`
}

/**
* Generates a random Title Case name by combining an adjective and noun.
*
* @param family - Theme to use for the random name (business or creative).
* @returns A Title Case random name (e.g. "Adaptive Vertical").
*/
export function getRandomTitleCaseName(family: RandomNameFamily = 'business'): string {
return capitalizeWords(getRandomName(family))
}

/**
* Given a string, it returns it with the first letter capitalized.
*
Expand Down
36 changes: 36 additions & 0 deletions packages/cli-kit/src/public/node/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,42 @@ describe('makeDirectoryWithRandomName', () => {
expect(takeRandomFromArray).toHaveBeenCalledTimes(4)
})
})

test('returns a Title Case name when titleCase option is true', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
vi.mocked(takeRandomFromArray).mockReturnValueOnce('adaptive')
vi.mocked(takeRandomFromArray).mockReturnValueOnce('vertical')

// When
const got = await generateRandomNameForSubdirectory({suffix: 'app', directory: tmpDir, titleCase: true})

// Then
expect(got).toEqual('Adaptive Vertical App')
})
})

test('checks hyphenated directory name for collisions when titleCase is true', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given - create a directory with the hyphenated version of the first attempt
vi.mocked(takeRandomFromArray).mockReturnValueOnce('taken')
vi.mocked(takeRandomFromArray).mockReturnValueOnce('name')
vi.mocked(takeRandomFromArray).mockReturnValueOnce('free')
vi.mocked(takeRandomFromArray).mockReturnValueOnce('name')

const content = 'test'
// The hyphenated form of "Taken Name App" is "taken-name-app"
const filePath = joinPath(tmpDir, 'taken-name-app')
await writeFile(filePath, content)

// When
const got = await generateRandomNameForSubdirectory({suffix: 'app', directory: tmpDir, titleCase: true})

// Then - should have rerolled and returned the second attempt
expect(got).toEqual('Free Name App')
expect(takeRandomFromArray).toHaveBeenCalledTimes(4)
})
})
})

describe('readFileSync', () => {
Expand Down
13 changes: 10 additions & 3 deletions packages/cli-kit/src/public/node/fs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {outputContent, outputToken, outputDebug} from './output.js'
import {joinPath, normalizePath} from './path.js'
import {OverloadParameters} from '../../private/common/ts/overloaded-parameters.js'
import {getRandomName, RandomNameFamily} from '../common/string.js'
import {capitalize, getRandomName, getRandomTitleCaseName, hyphenate, RandomNameFamily} from '../common/string.js'
import {systemTempDir} from '../../private/node/temp-dir.js'
import {
copy as fsCopy,
Expand Down Expand Up @@ -543,6 +543,9 @@ interface GenerateRandomDirectoryOptions {

/** Type of word to use for random name. */
family?: RandomNameFamily

/** If true, return a Title Case name instead of kebab-case. */
titleCase?: boolean
}

/**
Expand All @@ -553,8 +556,12 @@ interface GenerateRandomDirectoryOptions {
* @returns It returns the name of the directory.
*/
export async function generateRandomNameForSubdirectory(options: GenerateRandomDirectoryOptions): Promise<string> {
const generated = `${getRandomName(options.family ?? 'business')}-${options.suffix}`
const randomDirectoryPath = joinPath(options.directory, generated)
const family = options.family ?? 'business'
const baseName = options.titleCase ? getRandomTitleCaseName(family) : getRandomName(family)
const suffix = options.titleCase ? capitalize(options.suffix) : options.suffix
const generated = options.titleCase ? `${baseName} ${suffix}` : `${baseName}-${suffix}`
const directoryName = options.titleCase ? hyphenate(generated) : generated
const randomDirectoryPath = joinPath(options.directory, directoryName)
const isAppDirectoryTaken = await fileExists(randomDirectoryPath)

if (isAppDirectoryTaken) {
Expand Down
Loading