@tanstack/cli 0.59.8 → 0.60.1

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.
package/dist/dev-watch.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
3
4
  import chokidar from 'chokidar';
4
5
  import chalk from 'chalk';
5
6
  import { temporaryDirectory } from 'tempy';
6
- import { createApp, getFrameworkById, registerFramework, } from '@tanstack/create';
7
+ import { createApp, finalizeAddOns, getFrameworkById, registerFramework, scanAddOnDirectories, scanProjectDirectory, } from '@tanstack/create';
7
8
  import { FileSyncer } from './file-syncer.js';
8
9
  import { createUIEnvironment } from './ui-environment.js';
9
10
  class DebounceQueue {
@@ -42,6 +43,8 @@ export class DevWatchManager {
42
43
  this.tempDir = null;
43
44
  this.isBuilding = false;
44
45
  this.buildCount = 0;
46
+ this.appDevProcess = null;
47
+ this.lastSyncedSourceFiles = null;
45
48
  this.log = {
46
49
  tree: (prefix, msg, isLast = false) => {
47
50
  const connector = isLast ? '└─' : '├─';
@@ -78,12 +81,18 @@ export class DevWatchManager {
78
81
  console.log(chalk.bold('dev-watch'));
79
82
  this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`);
80
83
  this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`);
84
+ if (this.options.runDevCommand) {
85
+ this.log.tree('', `app dev server: ${chalk.cyan('enabled')}`);
86
+ }
81
87
  this.log.tree('', 'ready', true);
82
88
  // Setup signal handlers
83
89
  process.on('SIGINT', () => this.cleanup());
84
90
  process.on('SIGTERM', () => this.cleanup());
85
91
  // Start watching
86
92
  this.startWatcher();
93
+ if (this.options.runDevCommand) {
94
+ this.startAppDevServer();
95
+ }
87
96
  }
88
97
  async stop() {
89
98
  console.log();
@@ -145,17 +154,16 @@ export class DevWatchManager {
145
154
  try {
146
155
  this.log.section(`build #${buildId}`);
147
156
  const startTime = Date.now();
148
- if (!this.options.frameworkDefinitionInitializers) {
149
- throw new Error('There must be framework initalizers passed to frameworkDefinitionInitializers to use --dev-watch');
157
+ let refreshedFramework = this.createFrameworkDefinitionFromWatchPath();
158
+ if (!refreshedFramework && this.options.frameworkDefinitionInitializers) {
159
+ const refreshedFrameworks = this.options.frameworkDefinitionInitializers.map((frameworkInitalizer) => frameworkInitalizer());
160
+ refreshedFramework = refreshedFrameworks.find((f) => f.id === this.options.framework.id);
150
161
  }
151
- const refreshedFrameworks = this.options.frameworkDefinitionInitializers.map((frameworkInitalizer) => frameworkInitalizer());
152
- const refreshedFramework = refreshedFrameworks.find((f) => f.id === this.options.framework.id);
153
162
  if (!refreshedFramework) {
154
- throw new Error('Could not identify the framework');
163
+ throw new Error('Could not refresh framework from watch path or framework initializers');
155
164
  }
156
165
  // Update the chosen addons to use the latest code
157
166
  const chosenAddonIds = this.options.cliOptions.chosenAddOns.map((m) => m.id);
158
- const updatedChosenAddons = refreshedFramework.addOns.filter((f) => chosenAddonIds.includes(f.id));
159
167
  // Create temp directory for this build using tempy
160
168
  this.tempDir = temporaryDirectory();
161
169
  // Register the scanned framework
@@ -168,25 +176,33 @@ export class DevWatchManager {
168
176
  if (!registeredFramework) {
169
177
  throw new Error(`Failed to register framework: ${this.options.framework.id}`);
170
178
  }
171
- // Check if package.json was modified
172
- const packageJsonModified = Array.from(changes).some((filePath) => path.basename(filePath) === 'package.json');
179
+ const updatedChosenAddons = await finalizeAddOns(registeredFramework, this.options.cliOptions.mode, chosenAddonIds);
180
+ // Check if package metadata was modified
181
+ const packageMetadataChanged = Array.from(changes).some((filePath) => {
182
+ const normalized = filePath.replace(/\\/g, '/');
183
+ return /(^|\/)package\.json(\.ejs)?$/.test(normalized);
184
+ });
173
185
  const updatedOptions = {
174
186
  ...this.options.cliOptions,
175
187
  chosenAddOns: updatedChosenAddons,
176
188
  framework: registeredFramework,
177
189
  targetDir: this.tempDir,
178
190
  git: false,
179
- install: packageJsonModified,
191
+ install: packageMetadataChanged,
180
192
  };
181
193
  // Show package installation indicator if needed
182
- if (packageJsonModified) {
194
+ if (packageMetadataChanged) {
183
195
  this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`);
184
196
  }
185
197
  // Create app in temp directory with silent environment
186
198
  const silentEnvironment = createUIEnvironment(this.options.environment.appName, true);
187
199
  await createApp(silentEnvironment, updatedOptions);
188
200
  // Sync files to target directory
189
- const syncResult = await this.syncer.sync(this.tempDir, this.options.targetDir);
201
+ const syncResult = await this.syncer.sync(this.tempDir, this.options.targetDir, {
202
+ deleteRemoved: this.lastSyncedSourceFiles !== null,
203
+ previousSourceFiles: this.lastSyncedSourceFiles ?? undefined,
204
+ });
205
+ this.lastSyncedSourceFiles = new Set(syncResult.sourceFiles);
190
206
  // Clean up temp directory after sync is complete
191
207
  try {
192
208
  await fs.promises.rm(this.tempDir, { recursive: true, force: true });
@@ -197,18 +213,24 @@ export class DevWatchManager {
197
213
  const elapsed = Date.now() - startTime;
198
214
  // Build tree-style summary
199
215
  this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`);
200
- if (packageJsonModified) {
216
+ if (packageMetadataChanged) {
201
217
  this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`);
202
218
  }
203
219
  // Always show the last item in tree without checking for files to show
204
220
  const noMoreTreeItems = syncResult.updated.length === 0 &&
205
221
  syncResult.created.length === 0 &&
222
+ syncResult.deleted.length === 0 &&
206
223
  syncResult.errors.length === 0;
207
224
  if (syncResult.updated.length > 0) {
208
- this.log.tree(' ', `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`, syncResult.created.length === 0 && syncResult.errors.length === 0);
225
+ this.log.tree(' ', `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`, syncResult.created.length === 0 &&
226
+ syncResult.deleted.length === 0 &&
227
+ syncResult.errors.length === 0);
209
228
  }
210
229
  if (syncResult.created.length > 0) {
211
- this.log.tree(' ', `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`, syncResult.errors.length === 0);
230
+ this.log.tree(' ', `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`, syncResult.deleted.length === 0 && syncResult.errors.length === 0);
231
+ }
232
+ if (syncResult.deleted.length > 0) {
233
+ this.log.tree(' ', `deleted: ${chalk.green(syncResult.deleted.length + ' file' + (syncResult.deleted.length > 1 ? 's' : ''))}`, syncResult.errors.length === 0);
212
234
  }
213
235
  if (syncResult.errors.length > 0) {
214
236
  this.log.tree(' ', `failed: ${chalk.red(syncResult.errors.length + ' file' + (syncResult.errors.length > 1 ? 's' : ''))}`, true);
@@ -254,10 +276,16 @@ export class DevWatchManager {
254
276
  // Show created files
255
277
  if (syncResult.created.length > 0) {
256
278
  syncResult.created.forEach((file, index) => {
257
- const isLast = index === syncResult.created.length - 1;
279
+ const isLast = index === syncResult.created.length - 1 && syncResult.deleted.length === 0;
258
280
  this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast);
259
281
  });
260
282
  }
283
+ if (syncResult.deleted.length > 0) {
284
+ syncResult.deleted.forEach((file, index) => {
285
+ const isLast = index === syncResult.deleted.length - 1;
286
+ this.log.treeItem(' ', `${chalk.red('-')} ${file}`, isLast);
287
+ });
288
+ }
261
289
  // Always show errors
262
290
  if (syncResult.errors.length > 0) {
263
291
  console.log(); // Add spacing
@@ -273,6 +301,34 @@ export class DevWatchManager {
273
301
  this.isBuilding = false;
274
302
  }
275
303
  }
304
+ createFrameworkDefinitionFromWatchPath() {
305
+ const frameworkRoot = this.options.watchPath;
306
+ const projectDirectory = path.join(frameworkRoot, 'project');
307
+ const baseDirectory = path.join(projectDirectory, 'base');
308
+ if (!fs.existsSync(projectDirectory) || !fs.existsSync(baseDirectory)) {
309
+ return null;
310
+ }
311
+ const addOnDirectoryCandidates = [
312
+ path.join(frameworkRoot, 'add-ons'),
313
+ path.join(frameworkRoot, 'toolchains'),
314
+ path.join(frameworkRoot, 'examples'),
315
+ path.join(frameworkRoot, 'hosts'),
316
+ ];
317
+ const addOnDirectories = addOnDirectoryCandidates.filter((dir) => fs.existsSync(dir));
318
+ const addOns = addOnDirectories.length > 0 ? scanAddOnDirectories(addOnDirectories) : [];
319
+ const { files, basePackageJSON, optionalPackages } = scanProjectDirectory(projectDirectory, baseDirectory);
320
+ return {
321
+ id: this.options.framework.id,
322
+ name: this.options.framework.name,
323
+ description: this.options.framework.description,
324
+ version: this.options.framework.version,
325
+ base: files,
326
+ addOns,
327
+ basePackageJSON,
328
+ optionalPackages,
329
+ supportedModes: this.options.framework.supportedModes,
330
+ };
331
+ }
276
332
  cleanup() {
277
333
  console.log();
278
334
  console.log('Cleaning up...');
@@ -285,6 +341,51 @@ export class DevWatchManager {
285
341
  this.log.error(`Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`);
286
342
  }
287
343
  }
344
+ if (this.appDevProcess && !this.appDevProcess.killed) {
345
+ this.appDevProcess.kill('SIGTERM');
346
+ this.appDevProcess = null;
347
+ }
288
348
  process.exit(0);
289
349
  }
350
+ startAppDevServer() {
351
+ if (this.appDevProcess) {
352
+ return;
353
+ }
354
+ const { command, args } = this.getDevCommandForPackageManager(this.options.packageManager);
355
+ this.log.section('app dev server');
356
+ this.log.tree(' ', `starting: ${chalk.cyan([command, ...args].join(' '))}`);
357
+ this.appDevProcess = spawn(command, args, {
358
+ cwd: this.options.targetDir,
359
+ stdio: 'inherit',
360
+ shell: process.platform === 'win32',
361
+ env: process.env,
362
+ });
363
+ this.appDevProcess.on('exit', (code, signal) => {
364
+ if (signal) {
365
+ this.log.warning(`app dev server exited via signal ${signal}`);
366
+ }
367
+ else if (code && code !== 0) {
368
+ this.log.warning(`app dev server exited with code ${code}`);
369
+ }
370
+ this.appDevProcess = null;
371
+ });
372
+ this.appDevProcess.on('error', (error) => {
373
+ this.log.error(`Failed to start app dev server: ${error.message}`);
374
+ this.appDevProcess = null;
375
+ });
376
+ }
377
+ getDevCommandForPackageManager(packageManager) {
378
+ switch (packageManager) {
379
+ case 'npm':
380
+ return { command: 'npm', args: ['run', 'dev'] };
381
+ case 'yarn':
382
+ return { command: 'yarn', args: ['dev'] };
383
+ case 'bun':
384
+ return { command: 'bun', args: ['run', 'dev'] };
385
+ case 'deno':
386
+ return { command: 'deno', args: ['task', 'dev'] };
387
+ default:
388
+ return { command: 'pnpm', args: ['dev'] };
389
+ }
390
+ }
290
391
  }
@@ -3,11 +3,13 @@ import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import * as diff from 'diff';
5
5
  export class FileSyncer {
6
- async sync(sourceDir, targetDir) {
6
+ async sync(sourceDir, targetDir, options) {
7
7
  const result = {
8
8
  updated: [],
9
9
  skipped: [],
10
10
  created: [],
11
+ deleted: [],
12
+ sourceFiles: [],
11
13
  errors: [],
12
14
  };
13
15
  // Ensure directories exist
@@ -19,6 +21,10 @@ export class FileSyncer {
19
21
  }
20
22
  // Walk through source directory and sync files
21
23
  await this.syncDirectory(sourceDir, targetDir, sourceDir, result);
24
+ if (options?.deleteRemoved && options.previousSourceFiles) {
25
+ const currentSourceFileSet = new Set(result.sourceFiles);
26
+ await this.deleteRemovedFiles(targetDir, options.previousSourceFiles, currentSourceFileSet, result);
27
+ }
22
28
  return result;
23
29
  }
24
30
  async syncDirectory(currentPath, targetBase, sourceBase, result) {
@@ -46,6 +52,7 @@ export class FileSyncer {
46
52
  if (this.shouldSkipFile(entry.name)) {
47
53
  continue;
48
54
  }
55
+ result.sourceFiles.push(relativePath);
49
56
  try {
50
57
  const shouldUpdate = await this.shouldUpdateFile(sourcePath, targetPath);
51
58
  if (shouldUpdate) {
@@ -145,4 +152,26 @@ export class FileSyncer {
145
152
  const ext = path.extname(name).toLowerCase();
146
153
  return skipExtensions.includes(ext);
147
154
  }
155
+ async deleteRemovedFiles(targetDir, previousSourceFiles, currentSourceFiles, result) {
156
+ for (const relativePath of previousSourceFiles) {
157
+ if (currentSourceFiles.has(relativePath)) {
158
+ continue;
159
+ }
160
+ const targetPath = path.join(targetDir, relativePath);
161
+ try {
162
+ if (!fs.existsSync(targetPath)) {
163
+ continue;
164
+ }
165
+ const stats = await fs.promises.stat(targetPath);
166
+ if (!stats.isFile()) {
167
+ continue;
168
+ }
169
+ await fs.promises.unlink(targetPath);
170
+ result.deleted.push(relativePath);
171
+ }
172
+ catch (error) {
173
+ result.errors.push(`${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
174
+ }
175
+ }
176
+ }
148
177
  }
package/dist/index.js CHANGED
@@ -1 +1,15 @@
1
- export { cli } from './cli.js';
1
+ import { pathToFileURL } from 'node:url';
2
+ import { createReactFrameworkDefinition, createSolidFrameworkDefinition, } from '@tanstack/create';
3
+ import { cli } from './cli.js';
4
+ export { cli };
5
+ const entryPath = process.argv[1];
6
+ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
7
+ cli({
8
+ name: 'tanstack',
9
+ appName: 'TanStack',
10
+ frameworkDefinitionInitializers: [
11
+ createReactFrameworkDefinition,
12
+ createSolidFrameworkDefinition,
13
+ ],
14
+ });
15
+ }
package/dist/options.js CHANGED
@@ -4,7 +4,7 @@ import { getProjectName, promptForAddOnOptions, promptForEnvVars, selectAddOns,
4
4
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
5
5
  export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], showDeploymentOptions = false, }) {
6
6
  const options = {};
7
- options.framework = getFrameworkById(cliOptions.framework || 'react-cra');
7
+ options.framework = getFrameworkById(cliOptions.framework || 'react');
8
8
  // Validate project name
9
9
  if (cliOptions.projectName) {
10
10
  // Handle "." as project name - use sanitized current directory name
@@ -26,7 +26,10 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
26
26
  // Mode is always file-router (TanStack Start)
27
27
  options.mode = 'file-router';
28
28
  const template = cliOptions.template?.toLowerCase().trim();
29
- const routerOnly = !!cliOptions.routerOnly || (template ? template !== 'file-router' : false);
29
+ const isLegacyTemplate = template &&
30
+ ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(template);
31
+ const routerOnly = !!cliOptions.routerOnly ||
32
+ (isLegacyTemplate ? template !== 'file-router' : false);
30
33
  // TypeScript is always enabled with file-router
31
34
  options.typescript = true;
32
35
  // Package manager selection
@@ -1,11 +1,10 @@
1
1
  import type { FrameworkDefinition } from '@tanstack/create';
2
- export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, defaultRouterOnly, }: {
2
+ export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, defaultRouterOnly, }: {
3
3
  name: string;
4
4
  appName: string;
5
5
  forcedAddOns?: Array<string>;
6
6
  forcedDeployment?: string;
7
7
  defaultFramework?: string;
8
- webBase?: string;
9
8
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
10
9
  showDeploymentOptions?: boolean;
11
10
  legacyAutoCreate?: boolean;
@@ -5,6 +5,7 @@ export interface DevWatchOptions {
5
5
  framework: Framework;
6
6
  cliOptions: Options;
7
7
  packageManager: string;
8
+ runDevCommand?: boolean;
8
9
  environment: Environment;
9
10
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
10
11
  }
@@ -16,12 +17,17 @@ export declare class DevWatchManager {
16
17
  private tempDir;
17
18
  private isBuilding;
18
19
  private buildCount;
20
+ private appDevProcess;
21
+ private lastSyncedSourceFiles;
19
22
  constructor(options: DevWatchOptions);
20
23
  start(): Promise<void>;
21
24
  stop(): Promise<void>;
22
25
  private startWatcher;
23
26
  private handleChange;
24
27
  private rebuild;
28
+ private createFrameworkDefinitionFromWatchPath;
25
29
  private cleanup;
30
+ private startAppDevServer;
31
+ private getDevCommandForPackageManager;
26
32
  private log;
27
33
  }
@@ -6,13 +6,20 @@ export interface SyncResult {
6
6
  updated: Array<FileUpdate>;
7
7
  skipped: Array<string>;
8
8
  created: Array<string>;
9
+ deleted: Array<string>;
10
+ sourceFiles: Array<string>;
9
11
  errors: Array<string>;
10
12
  }
13
+ export interface SyncOptions {
14
+ deleteRemoved?: boolean;
15
+ previousSourceFiles?: Set<string>;
16
+ }
11
17
  export declare class FileSyncer {
12
- sync(sourceDir: string, targetDir: string): Promise<SyncResult>;
18
+ sync(sourceDir: string, targetDir: string, options?: SyncOptions): Promise<SyncResult>;
13
19
  private syncDirectory;
14
20
  private shouldUpdateFile;
15
21
  private calculateHash;
16
22
  private shouldSkipDirectory;
17
23
  private shouldSkipFile;
24
+ private deleteRemovedFiles;
18
25
  }
@@ -1 +1,2 @@
1
- export { cli } from './cli.js';
1
+ import { cli } from './cli.js';
2
+ export { cli };
@@ -12,10 +12,11 @@ export interface CliOptions {
12
12
  mcp?: boolean;
13
13
  mcpSse?: boolean;
14
14
  starter?: string;
15
+ templateId?: string;
15
16
  targetDir?: string;
16
17
  interactive?: boolean;
17
- ui?: boolean;
18
18
  devWatch?: string;
19
+ runDev?: boolean;
19
20
  install?: boolean;
20
21
  addOnConfig?: string;
21
22
  force?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cli",
3
- "version": "0.59.8",
3
+ "version": "0.60.1",
4
4
  "description": "TanStack CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,10 +38,10 @@
38
38
  "tempy": "^3.1.0",
39
39
  "validate-npm-package-name": "^7.0.0",
40
40
  "zod": "^3.24.2",
41
- "@tanstack/create": "0.61.6",
42
- "@tanstack/create-ui": "0.59.8"
41
+ "@tanstack/create": "0.62.1"
43
42
  },
44
43
  "devDependencies": {
44
+ "@playwright/test": "^1.58.2",
45
45
  "@tanstack/config": "^0.16.2",
46
46
  "@types/diff": "^5.2.0",
47
47
  "@types/express": "^5.0.1",
@@ -57,6 +57,11 @@
57
57
  "scripts": {
58
58
  "build": "tsc",
59
59
  "dev": "tsc --watch",
60
+ "test:e2e": "npm run test:e2e:blocking",
61
+ "test:e2e:blocking": "npm run build && playwright test --grep @blocking",
62
+ "test:e2e:matrix": "npm run build && playwright test --grep @matrix",
63
+ "test:e2e:debug": "npm run build && playwright test --debug",
64
+ "test:e2e:headed": "npm run build && playwright test --headed",
60
65
  "test:lint": "eslint ./src",
61
66
  "test": "vitest run",
62
67
  "test:watch": "vitest",