diff --git a/packages/cta-cli/package.json b/packages/cta-cli/package.json index f811f24c..fb8c206b 100644 --- a/packages/cta-cli/package.json +++ b/packages/cta-cli/package.json @@ -40,6 +40,7 @@ "commander": "^13.1.0", "express": "^4.21.2", "semver": "^7.7.2", + "validate-npm-package-name": "^7.0.0", "zod": "^3.24.2" }, "devDependencies": { @@ -47,6 +48,7 @@ "@types/express": "^5.0.1", "@types/node": "^22.13.4", "@types/semver": "^7.7.0", + "@types/validate-npm-package-name": "^4.0.2", "@vitest/coverage-v8": "3.1.1", "eslint": "^9.20.0", "typescript": "^5.6.3", diff --git a/packages/cta-cli/src/command-line.ts b/packages/cta-cli/src/command-line.ts index 3a43569a..852b1864 100644 --- a/packages/cta-cli/src/command-line.ts +++ b/packages/cta-cli/src/command-line.ts @@ -12,6 +12,7 @@ import { import type { Options } from '@tanstack/cta-engine' import type { CliOptions } from './types.js' +import { validateProjectName } from './utils.js' export async function normalizeOptions( cliOptions: CliOptions, @@ -27,6 +28,15 @@ export async function normalizeOptions( return undefined } + // Validate project name + if (projectName) { + const { valid, error } = validateProjectName(projectName) + if (!valid) { + console.error(error); + process.exit(1); + } + } + let tailwind = !!cliOptions.tailwind let mode: string = diff --git a/packages/cta-cli/src/options.ts b/packages/cta-cli/src/options.ts index b5383ea0..443c88ad 100644 --- a/packages/cta-cli/src/options.ts +++ b/packages/cta-cli/src/options.ts @@ -24,6 +24,7 @@ import { import type { Options } from '@tanstack/cta-engine' import type { CliOptions } from './types.js' +import { validateProjectName } from './utils.js' export async function promptForCreateOptions( cliOptions: CliOptions, @@ -41,7 +42,16 @@ export async function promptForCreateOptions( options.framework = getFrameworkById(cliOptions.framework || 'react-cra')! - options.projectName = cliOptions.projectName || (await getProjectName()) + if (cliOptions.projectName) { + const { valid, error } = validateProjectName(cliOptions.projectName) + if (!valid) { + console.error(error) + process.exit(1) + } + options.projectName = cliOptions.projectName + } else { + options.projectName = await getProjectName() + } // Router type selection if (forcedMode) { diff --git a/packages/cta-cli/src/ui-prompts.ts b/packages/cta-cli/src/ui-prompts.ts index 35d0305d..a2953755 100644 --- a/packages/cta-cli/src/ui-prompts.ts +++ b/packages/cta-cli/src/ui-prompts.ts @@ -17,6 +17,7 @@ import type { AddOn, PackageManager } from '@tanstack/cta-engine' import type { Framework } from '@tanstack/cta-engine/dist/types/types.js' import { InitialData } from '../../cta-ui/src/types' +import { validateProjectName } from './utils.js' export async function getProjectName(): Promise { const value = await text({ @@ -26,6 +27,11 @@ export async function getProjectName(): Promise { if (!value) { return 'Please enter a name' } + + const { valid, error } = validateProjectName(value); + if (!valid) { + return error; + } }, }) diff --git a/packages/cta-cli/src/utils.ts b/packages/cta-cli/src/utils.ts index 09a04bae..21da6274 100644 --- a/packages/cta-cli/src/utils.ts +++ b/packages/cta-cli/src/utils.ts @@ -1,4 +1,5 @@ import type { TemplateOptions } from './types.js' +import validatePackageName from "validate-npm-package-name"; export function convertTemplateToMode(template: TemplateOptions): string { if (template === 'typescript' || template === 'javascript') { @@ -6,3 +7,18 @@ export function convertTemplateToMode(template: TemplateOptions): string { } return 'file-router' } + +export function validateProjectName(name: string) { + const { + validForNewPackages, + validForOldPackages, + errors, + warnings, + } = validatePackageName(name); + const error = errors?.[0] || warnings?.[0]; + + return { + valid: validForNewPackages && validForOldPackages, + error: error?.replace('name', 'Project name') || 'Invalid project name', + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0edf653..12a2c07f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,13 +361,16 @@ importers: semver: specifier: ^7.7.2 version: 7.7.2 + validate-npm-package-name: + specifier: ^7.0.0 + version: 7.0.0 zod: specifier: ^3.24.2 version: 3.24.3 devDependencies: '@tanstack/config': specifier: ^0.16.2 - version: 0.16.3(@types/node@22.15.3)(esbuild@0.25.8)(eslint@9.25.1(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) + version: 0.16.3(@types/node@22.15.3)(esbuild@0.25.8)(eslint@9.25.1(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -377,6 +380,9 @@ importers: '@types/semver': specifier: ^7.7.0 version: 7.7.0 + '@types/validate-npm-package-name': + specifier: ^4.0.2 + version: 4.0.2 '@vitest/coverage-v8': specifier: 3.1.1 version: 3.1.1(vitest@3.1.2(@types/node@22.15.3)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) @@ -2085,6 +2091,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@typescript-eslint/eslint-plugin@8.31.1': resolution: {integrity: sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4669,6 +4678,10 @@ packages: resolution: {integrity: sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==} engines: {node: '>= 10.13.0'} + validate-npm-package-name@7.0.0: + resolution: {integrity: sha512-bwVk/OK+Qu108aJcMAEiU4yavHUI7aN20TgZNBj9MR2iU1zPUl1Z1Otr7771ExfYTPTvfN8ZJ1pbr5Iklgt4xg==} + engines: {node: ^20.17.0 || >=22.9.0} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -6357,6 +6370,41 @@ snapshots: tailwindcss: 4.1.6 vite: 7.1.7(@types/node@24.6.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + '@tanstack/config@0.16.3(@types/node@22.15.3)(esbuild@0.25.8)(eslint@9.25.1(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))': + dependencies: + '@commitlint/parse': 19.8.0 + '@eslint/js': 9.25.1 + '@stylistic/eslint-plugin-js': 4.2.0(eslint@9.25.1(jiti@2.5.1)) + commander: 13.1.0 + esbuild-register: 3.6.0(esbuild@0.25.8) + eslint-plugin-import-x: 4.11.0(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3) + eslint-plugin-n: 17.17.0(eslint@9.25.1(jiti@2.5.1)) + globals: 16.0.0 + interpret: 3.1.1 + jsonfile: 6.1.0 + liftoff: 5.0.0 + minimist: 1.2.8 + rollup-plugin-preserve-directives: 0.4.0(rollup@4.46.2) + semver: 7.7.2 + simple-git: 3.27.0 + typedoc: 0.27.9(typescript@5.8.3) + typedoc-plugin-frontmatter: 1.3.0(typedoc-plugin-markdown@4.6.3(typedoc@0.27.9(typescript@5.8.3))) + typedoc-plugin-markdown: 4.6.3(typedoc@0.27.9(typescript@5.8.3)) + typescript-eslint: 8.31.1(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3) + v8flags: 4.0.1 + vite-plugin-dts: 4.2.3(@types/node@22.15.3)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) + vite-plugin-externalize-deps: 0.9.0(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) + vite-tsconfig-paths: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)) + vue-eslint-parser: 9.4.3(eslint@9.25.1(jiti@2.5.1)) + transitivePeerDependencies: + - '@types/node' + - esbuild + - eslint + - rollup + - supports-color + - typescript + - vite + '@tanstack/config@0.16.3(@types/node@22.15.3)(esbuild@0.25.8)(eslint@9.25.1(jiti@2.5.1))(rollup@4.46.2)(typescript@5.8.3)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))': dependencies: '@commitlint/parse': 19.8.0 @@ -6569,6 +6617,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/validate-npm-package-name@4.0.2': {} + '@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3))(eslint@9.25.1(jiti@2.5.1))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -9278,6 +9328,8 @@ snapshots: v8flags@4.0.1: {} + validate-npm-package-name@7.0.0: {} + vary@1.1.2: {} vite-node@3.1.2(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1): @@ -9343,6 +9395,25 @@ snapshots: - tsx - yaml + vite-plugin-dts@4.2.3(@types/node@22.15.3)(rollup@4.46.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): + dependencies: + '@microsoft/api-extractor': 7.47.7(@types/node@22.15.3) + '@rollup/pluginutils': 5.1.4(rollup@4.46.2) + '@volar/typescript': 2.4.13 + '@vue/language-core': 2.1.6(typescript@5.8.3) + compare-versions: 6.1.1 + debug: 4.4.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + magic-string: 0.30.17 + typescript: 5.8.3 + optionalDependencies: + vite: 6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-dts@4.2.3(@types/node@22.15.3)(rollup@4.46.2)(typescript@5.8.3)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@22.15.3) @@ -9381,6 +9452,10 @@ snapshots: - rollup - supports-color + vite-plugin-externalize-deps@0.9.0(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): + dependencies: + vite: 6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + vite-plugin-externalize-deps@0.9.0(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): dependencies: vite: 7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) @@ -9389,6 +9464,17 @@ snapshots: dependencies: vite: 7.1.7(@types/node@24.6.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): + dependencies: + debug: 4.4.0 + globrex: 0.1.2 + tsconfck: 3.1.5(typescript@5.8.3) + optionalDependencies: + vite: 6.3.5(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + transitivePeerDependencies: + - supports-color + - typescript + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@24.6.0)(jiti@2.5.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)): dependencies: debug: 4.4.0