@tanstack/cta-cli 0.37.1 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,18 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/cta-engine';
3
+ import { validateProjectName } from './utils.js';
3
4
  export async function normalizeOptions(cliOptions, forcedMode, forcedAddOns, opts) {
4
5
  const projectName = (cliOptions.projectName ?? '').trim();
5
6
  if (!projectName && !opts?.disableNameCheck) {
6
7
  return undefined;
7
8
  }
9
+ if (projectName) {
10
+ const { valid, error } = validateProjectName(projectName);
11
+ if (!valid) {
12
+ console.error(error);
13
+ process.exit(1);
14
+ }
15
+ }
8
16
  let tailwind = !!cliOptions.tailwind;
9
17
  let mode = forcedMode ||
10
18
  (cliOptions.template === 'file-router' ? 'file-router' : 'code-router');
package/dist/options.js CHANGED
@@ -1,10 +1,21 @@
1
1
  import { intro } from '@clack/prompts';
2
2
  import { finalizeAddOns, getFrameworkById, getPackageManager, populateAddOnOptionsDefaults, readConfigFile, } from '@tanstack/cta-engine';
3
3
  import { getProjectName, promptForAddOnOptions, selectAddOns, selectGit, selectDeployment, selectPackageManager, selectRouterType, selectTailwind, selectToolchain, selectTypescript, } from './ui-prompts.js';
4
+ import { validateProjectName } from './utils.js';
4
5
  export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], forcedMode, showDeploymentOptions = false, }) {
5
6
  const options = {};
6
7
  options.framework = getFrameworkById(cliOptions.framework || 'react-cra');
7
- options.projectName = cliOptions.projectName || (await getProjectName());
8
+ if (cliOptions.projectName) {
9
+ const { valid, error } = validateProjectName(cliOptions.projectName);
10
+ if (!valid) {
11
+ console.error(error);
12
+ process.exit(1);
13
+ }
14
+ options.projectName = cliOptions.projectName;
15
+ }
16
+ else {
17
+ options.projectName = await getProjectName();
18
+ }
8
19
  // Router type selection
9
20
  if (forcedMode) {
10
21
  options.mode = forcedMode;
@@ -1,2 +1,6 @@
1
1
  import type { TemplateOptions } from './types.js';
2
2
  export declare function convertTemplateToMode(template: TemplateOptions): string;
3
+ export declare function validateProjectName(name: string): {
4
+ valid: boolean;
5
+ error: string;
6
+ };
@@ -1,5 +1,6 @@
1
- import { cancel, confirm, isCancel, multiselect, select, text, } from '@clack/prompts';
1
+ import { cancel, confirm, isCancel, multiselect, note, select, text, } from '@clack/prompts';
2
2
  import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getAllAddOns, } from '@tanstack/cta-engine';
3
+ import { validateProjectName } from './utils.js';
3
4
  export async function getProjectName() {
4
5
  const value = await text({
5
6
  message: 'What would you like to name your project?',
@@ -8,6 +9,10 @@ export async function getProjectName() {
8
9
  if (!value) {
9
10
  return 'Please enter a name';
10
11
  }
12
+ const { valid, error } = validateProjectName(value);
13
+ if (!valid) {
14
+ return error;
15
+ }
11
16
  },
12
17
  });
13
18
  if (isCancel(value)) {
@@ -74,12 +79,19 @@ export async function selectPackageManager() {
74
79
  }
75
80
  return packageManager;
76
81
  }
82
+ // Track if we've shown the multiselect help text
83
+ let hasShownMultiselectHelp = false;
77
84
  export async function selectAddOns(framework, mode, type, message, forcedAddOns = []) {
78
85
  const allAddOns = await getAllAddOns(framework, mode);
79
86
  const addOns = allAddOns.filter((addOn) => addOn.type === type);
80
87
  if (addOns.length === 0) {
81
88
  return [];
82
89
  }
90
+ // Show help text only once
91
+ if (!hasShownMultiselectHelp) {
92
+ note('Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm', 'Keyboard Shortcuts');
93
+ hasShownMultiselectHelp = true;
94
+ }
83
95
  const value = await multiselect({
84
96
  message,
85
97
  options: addOns
package/dist/utils.js CHANGED
@@ -1,6 +1,16 @@
1
+ import validatePackageName from 'validate-npm-package-name';
1
2
  export function convertTemplateToMode(template) {
2
3
  if (template === 'typescript' || template === 'javascript') {
3
4
  return 'code-router';
4
5
  }
5
6
  return 'file-router';
6
7
  }
8
+ export function validateProjectName(name) {
9
+ const { validForNewPackages, validForOldPackages, errors, warnings } = validatePackageName(name);
10
+ const error = errors?.[0] || warnings?.[0];
11
+ return {
12
+ valid: validForNewPackages && validForOldPackages,
13
+ error: error?.replace(/name/g, 'Project name') ||
14
+ 'Project name does not meet npm package naming requirements',
15
+ };
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-cli",
3
- "version": "0.37.1",
3
+ "version": "0.40.0",
4
4
  "description": "Tanstack Application Builder CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,15 +29,17 @@
29
29
  "commander": "^13.1.0",
30
30
  "express": "^4.21.2",
31
31
  "semver": "^7.7.2",
32
+ "validate-npm-package-name": "^7.0.0",
32
33
  "zod": "^3.24.2",
33
- "@tanstack/cta-engine": "0.37.1",
34
- "@tanstack/cta-ui": "0.37.1"
34
+ "@tanstack/cta-engine": "0.40.0",
35
+ "@tanstack/cta-ui": "0.40.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@tanstack/config": "^0.16.2",
38
39
  "@types/express": "^5.0.1",
39
40
  "@types/node": "^22.13.4",
40
41
  "@types/semver": "^7.7.0",
42
+ "@types/validate-npm-package-name": "^4.0.2",
41
43
  "@vitest/coverage-v8": "3.1.1",
42
44
  "eslint": "^9.20.0",
43
45
  "typescript": "^5.6.3",
@@ -9,6 +9,7 @@ import {
9
9
  populateAddOnOptionsDefaults,
10
10
  } from '@tanstack/cta-engine'
11
11
 
12
+ import { validateProjectName } from './utils.js'
12
13
  import type { Options } from '@tanstack/cta-engine'
13
14
 
14
15
  import type { CliOptions } from './types.js'
@@ -27,6 +28,14 @@ export async function normalizeOptions(
27
28
  return undefined
28
29
  }
29
30
 
31
+ if (projectName) {
32
+ const { valid, error } = validateProjectName(projectName)
33
+ if (!valid) {
34
+ console.error(error)
35
+ process.exit(1)
36
+ }
37
+ }
38
+
30
39
  let tailwind = !!cliOptions.tailwind
31
40
 
32
41
  let mode: string =
package/src/options.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  selectTypescript,
22
22
  } from './ui-prompts.js'
23
23
 
24
+ import { validateProjectName } from './utils.js'
24
25
  import type { Options } from '@tanstack/cta-engine'
25
26
 
26
27
  import type { CliOptions } from './types.js'
@@ -41,7 +42,16 @@ export async function promptForCreateOptions(
41
42
 
42
43
  options.framework = getFrameworkById(cliOptions.framework || 'react-cra')!
43
44
 
44
- options.projectName = cliOptions.projectName || (await getProjectName())
45
+ if (cliOptions.projectName) {
46
+ const { valid, error } = validateProjectName(cliOptions.projectName)
47
+ if (!valid) {
48
+ console.error(error)
49
+ process.exit(1)
50
+ }
51
+ options.projectName = cliOptions.projectName
52
+ } else {
53
+ options.projectName = await getProjectName()
54
+ }
45
55
 
46
56
  // Router type selection
47
57
  if (forcedMode) {
package/src/ui-prompts.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  confirm,
4
4
  isCancel,
5
5
  multiselect,
6
+ note,
6
7
  select,
7
8
  text,
8
9
  } from '@clack/prompts'
@@ -13,6 +14,7 @@ import {
13
14
  getAllAddOns,
14
15
  } from '@tanstack/cta-engine'
15
16
 
17
+ import { validateProjectName } from './utils.js'
16
18
  import type { AddOn, PackageManager } from '@tanstack/cta-engine'
17
19
 
18
20
  import type { Framework } from '@tanstack/cta-engine/dist/types/types.js'
@@ -26,6 +28,11 @@ export async function getProjectName(): Promise<string> {
26
28
  if (!value) {
27
29
  return 'Please enter a name'
28
30
  }
31
+
32
+ const { valid, error } = validateProjectName(value)
33
+ if (!valid) {
34
+ return error
35
+ }
29
36
  },
30
37
  })
31
38
 
@@ -103,6 +110,9 @@ export async function selectPackageManager(): Promise<PackageManager> {
103
110
  return packageManager
104
111
  }
105
112
 
113
+ // Track if we've shown the multiselect help text
114
+ let hasShownMultiselectHelp = false
115
+
106
116
  export async function selectAddOns(
107
117
  framework: Framework,
108
118
  mode: string,
@@ -116,6 +126,12 @@ export async function selectAddOns(
116
126
  return []
117
127
  }
118
128
 
129
+ // Show help text only once
130
+ if (!hasShownMultiselectHelp) {
131
+ note('Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm', 'Keyboard Shortcuts')
132
+ hasShownMultiselectHelp = true
133
+ }
134
+
119
135
  const value = await multiselect({
120
136
  message,
121
137
  options: addOns
package/src/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import validatePackageName from 'validate-npm-package-name'
1
2
  import type { TemplateOptions } from './types.js'
2
3
 
3
4
  export function convertTemplateToMode(template: TemplateOptions): string {
@@ -6,3 +7,16 @@ export function convertTemplateToMode(template: TemplateOptions): string {
6
7
  }
7
8
  return 'file-router'
8
9
  }
10
+
11
+ export function validateProjectName(name: string) {
12
+ const { validForNewPackages, validForOldPackages, errors, warnings } =
13
+ validatePackageName(name)
14
+ const error = errors?.[0] || warnings?.[0]
15
+
16
+ return {
17
+ valid: validForNewPackages && validForOldPackages,
18
+ error:
19
+ error?.replace(/name/g, 'Project name') ||
20
+ 'Project name does not meet npm package naming requirements',
21
+ }
22
+ }
@@ -93,7 +93,8 @@ describe('selectPackageManager', () => {
93
93
  })
94
94
 
95
95
  describe('selectAddOns', () => {
96
- it('should select some add-ons', async () => {
96
+ it('should show keyboard shortcuts help and select add-ons', async () => {
97
+ const noteSpy = vi.spyOn(clack, 'note').mockImplementation(() => {})
97
98
  vi.spyOn(clack, 'multiselect').mockImplementation(async () => ['add-on-1'])
98
99
  vi.spyOn(clack, 'isCancel').mockImplementation(() => false)
99
100
 
@@ -114,7 +115,12 @@ describe('selectAddOns', () => {
114
115
  'add-on',
115
116
  'Select add-ons',
116
117
  )
118
+
117
119
  expect(packageManager).toEqual(['add-on-1'])
120
+ expect(noteSpy).toHaveBeenCalledWith(
121
+ 'Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm',
122
+ 'Keyboard Shortcuts',
123
+ )
118
124
  })
119
125
 
120
126
  it('should exit on cancel', async () => {