Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/current-directory-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/cli": minor
---

Support initializing a project in the current directory from the create prompt or by passing `.` as the project name.
20 changes: 11 additions & 9 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
getTelemetryStatus,
setTelemetryEnabled,
} from './telemetry-config.js'
import { TelemetryClient, createTelemetryClient } from './telemetry.js'
import { createTelemetryClient } from './telemetry.js'

import { promptForAddOns, promptForCreateOptions } from './options.js'
import {
Expand All @@ -43,6 +43,7 @@ import { createUIEnvironment } from './ui-environment.js'
import { DevWatchManager } from './dev-watch.js'

import type { CliOptions } from './types.js'
import type { TelemetryClient } from './telemetry.js'
import type {
FrameworkDefinition,
Options,
Expand Down Expand Up @@ -500,8 +501,7 @@ export function cli({
getResolvedCreateTelemetryProperties(normalizedOpts, options),
)

normalizedOpts.targetDir =
options.targetDir || resolve(process.cwd(), projectName)
normalizedOpts.targetDir = resolve(normalizedOpts.targetDir)

// Create the initial app with minimal output for dev watch mode
console.log(chalk.bold('\ndev-watch'))
Expand Down Expand Up @@ -850,7 +850,11 @@ export function cli({

let cameFromPrompts = false
if (finalOptions) {
intro(`Creating a new ${appName} app in ${projectName}...`)
const createLocation =
resolve(finalOptions.targetDir) === resolve(process.cwd())
? 'the current directory'
: finalOptions.projectName
intro(`Creating a new ${appName} app in ${createLocation}...`)
} else {
if (!wantsInteractiveMode) {
throw new Error(
Expand Down Expand Up @@ -880,12 +884,10 @@ export function cli({
;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
!!cliOptions.routerOnly

if (options.targetDir) {
finalOptions.targetDir = options.targetDir
} else if (finalOptions.targetDir) {
if (finalOptions.targetDir) {
// Keep the normalized target dir.
} else if (projectName === '.') {
finalOptions.targetDir = resolve(process.cwd())
} else if (options.targetDir) {
finalOptions.targetDir = resolve(options.targetDir)
} else {
finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName)
}
Expand Down
22 changes: 9 additions & 13 deletions packages/cli/src/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
} from '@tanstack/create'

import {
getCurrentDirectoryName,
sanitizePackageName,
resolveProjectLocation,
validateProjectName,
} from './utils.js'
import type { Options } from '@tanstack/create'
Expand Down Expand Up @@ -406,21 +405,18 @@ export async function normalizeOptions(
forcedDeployment?: string
},
): Promise<Options | undefined> {
let projectName = (cliOptions.projectName ?? '').trim()
let targetDir: string
const projectLocation = resolveProjectLocation({
projectName: cliOptions.projectName,
targetDir: cliOptions.targetDir,
})

// Handle "." as project name - use current directory
if (projectName === '.') {
projectName = sanitizePackageName(getCurrentDirectoryName())
targetDir = resolve(process.cwd())
} else {
targetDir = resolve(process.cwd(), projectName)
}

if (!projectName && !opts?.disableNameCheck) {
if (!projectLocation && !opts?.disableNameCheck) {
return undefined
}

const projectName = projectLocation?.projectName ?? ''
const targetDir = projectLocation?.targetDir ?? resolve(process.cwd())

if (projectName) {
const { valid, error } = validateProjectName(projectName)
if (!valid) {
Expand Down
35 changes: 18 additions & 17 deletions packages/cli/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import {
} from './command-line.js'

import {
getCurrentDirectoryName,
sanitizePackageName,
resolveProjectLocation,
validateProjectName,
} from './utils.js'
import type { Options } from '@tanstack/create'
Expand Down Expand Up @@ -68,21 +67,23 @@ export async function promptForCreateOptions(
}
}

// Validate project name
if (cliOptions.projectName) {
// Handle "." as project name - use sanitized current directory name
if (cliOptions.projectName === '.') {
options.projectName = sanitizePackageName(getCurrentDirectoryName())
} else {
options.projectName = cliOptions.projectName
}
const { valid, error } = validateProjectName(options.projectName)
if (!valid) {
console.error(error)
process.exit(1)
}
} else {
options.projectName = await getProjectName()
const projectLocation = resolveProjectLocation({
projectName: cliOptions.projectName ?? (await getProjectName()),
targetDir: cliOptions.targetDir,
emptyProjectNameIsCurrentDirectory: true,
})

if (!projectLocation) {
throw new Error('Project name or target directory is required')
}

options.projectName = projectLocation.projectName
options.targetDir = projectLocation.targetDir

const { valid, error } = validateProjectName(options.projectName)
if (!valid) {
console.error(error)
process.exit(1)
}

// Mode is always file-router (TanStack Start)
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { version as nodeVersion } from 'node:process'

import {
TELEMETRY_NOTICE_VERSION,
getTelemetryStatus,
markTelemetryNoticeSeen,
TELEMETRY_NOTICE_VERSION,
} from './telemetry-config.js'

import type { StatusEvent, StatusStepType } from '@tanstack/create'
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/ui-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
import chalk from 'chalk'

import { createDefaultEnvironment } from '@tanstack/create'
import type { StatusEvent } from '@tanstack/create'

import type { Environment } from '@tanstack/create'
import type { Environment, StatusEvent } from '@tanstack/create'
import type { TelemetryClient } from './telemetry.js'

export function createUIEnvironment(
Expand Down
56 changes: 23 additions & 33 deletions packages/cli/src/ui-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
getAllAddOns,
} from '@tanstack/create'

import { validateProjectName } from './utils.js'
import {
isCurrentDirectoryProjectNameInput,
validateProjectName,
} from './utils.js'
import type { AddOn, PackageManager } from '@tanstack/create'

import type { Framework } from '@tanstack/create/dist/types/types.js'
Expand All @@ -29,7 +32,7 @@ export async function selectFramework(
frameworks.find(
(f) => f.id.toLowerCase() === defaultFrameworkId.toLowerCase(),
)?.id) ||
frameworks[0]!.id
frameworks[0].id

const selected = await select({
message: 'Select framework:',
Expand Down Expand Up @@ -64,10 +67,10 @@ export async function selectInstall(): Promise<boolean> {
export async function getProjectName(): Promise<string> {
const value = await text({
message: 'What would you like to name your project?',
defaultValue: 'my-app',
placeholder: 'Leave empty to initialize in the current directory',
validate(value) {
if (!value) {
return 'Please enter a name'
if (isCurrentDirectoryProjectNameInput(value)) {
return
}

const { valid, error } = validateProjectName(value)
Expand All @@ -82,7 +85,7 @@ export async function getProjectName(): Promise<string> {
process.exit(0)
}

return value
return value.trim()
}

export async function selectPackageManager(): Promise<PackageManager> {
Expand Down Expand Up @@ -284,34 +287,21 @@ export async function promptForAddOnOptions(
addOnOptions[addOnId] = {}

for (const [optionName, option] of Object.entries(addOn.options)) {
if (option && typeof option === 'object' && 'type' in option) {
if (option.type === 'select') {
const selectOption = option as {
type: 'select'
label: string
description?: string
default: string
options: Array<{ value: string; label: string }>
}

const value = await select({
message: `${addOn.name}: ${selectOption.label}`,
options: selectOption.options.map((opt) => ({
value: opt.value,
label: opt.label,
})),
initialValue: selectOption.default,
})

if (isCancel(value)) {
cancel('Operation cancelled.')
process.exit(0)
}

addOnOptions[addOnId][optionName] = value
}
// Future option types can be added here
const value = await select({
message: `${addOn.name}: ${option.label}`,
options: option.options.map((opt) => ({
value: opt.value,
label: opt.label,
})),
initialValue: option.default,
})

if (isCancel(value)) {
cancel('Operation cancelled.')
process.exit(0)
}

addOnOptions[addOnId][optionName] = value
}
}

Expand Down
62 changes: 61 additions & 1 deletion packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { basename } from 'node:path'
import { basename, resolve } from 'node:path'
import validatePackageName from 'validate-npm-package-name'

const FALLBACK_PACKAGE_NAME = 'tanstack-app'

export function sanitizePackageName(name: string): string {
return name
.toLowerCase()
Expand All @@ -16,6 +18,64 @@ export function getCurrentDirectoryName(): string {
return basename(process.cwd())
}

export function getDirectoryPackageName(directory: string): string {
return sanitizePackageName(basename(resolve(directory))) || FALLBACK_PACKAGE_NAME
}

export function getCurrentDirectoryPackageName(): string {
return getDirectoryPackageName(process.cwd())
}

export function isCurrentDirectoryProjectNameInput(name: string): boolean {
const normalized = name.trim()
return normalized === '' || normalized === '.'
}

export function resolveProjectLocation({
projectName,
targetDir,
emptyProjectNameIsCurrentDirectory = false,
}: {
projectName?: string
targetDir?: string
emptyProjectNameIsCurrentDirectory?: boolean
}): { projectName: string; targetDir: string } | undefined {
const normalizedProjectName = projectName?.trim() ?? ''

if (normalizedProjectName === '.') {
return {
projectName: getCurrentDirectoryPackageName(),
targetDir: resolve(process.cwd()),
}
}

if (normalizedProjectName) {
return {
projectName: normalizedProjectName,
targetDir: targetDir
? resolve(targetDir)
: resolve(process.cwd(), normalizedProjectName),
}
}

if (targetDir) {
const resolvedTargetDir = resolve(targetDir)
return {
projectName: getDirectoryPackageName(resolvedTargetDir),
targetDir: resolvedTargetDir,
}
}

if (emptyProjectNameIsCurrentDirectory) {
return {
projectName: getCurrentDirectoryPackageName(),
targetDir: resolve(process.cwd()),
}
}

return undefined
}

export function validateProjectName(name: string) {
const { validForNewPackages, validForOldPackages, errors, warnings } =
validatePackageName(name)
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/tests/command-line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ describe('normalizeOptions', () => {
expect(options?.targetDir).toBe(resolve(process.cwd()))
})

it('should derive the project name from target-dir when no name is provided', async () => {
const options = await normalizeOptions({
targetDir: 'my-target-app',
})

expect(options?.projectName).toBe('my-target-app')
expect(options?.targetDir).toBe(resolve(process.cwd(), 'my-target-app'))
})

it('should always enable typescript (file-router/TanStack Start requires it)', async () => {
const options = await normalizeOptions({
projectName: 'test',
Expand Down
Loading
Loading