@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/src/dev-watch.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { spawn } from 'node:child_process'
3
4
 
4
5
  import chokidar from 'chokidar'
5
6
  import chalk from 'chalk'
6
7
  import { temporaryDirectory } from 'tempy'
7
8
  import {
8
9
  createApp,
10
+ finalizeAddOns,
9
11
  getFrameworkById,
10
12
  registerFramework,
13
+ scanAddOnDirectories,
14
+ scanProjectDirectory,
11
15
  } from '@tanstack/create'
12
16
  import { FileSyncer } from './file-syncer.js'
13
17
  import { createUIEnvironment } from './ui-environment.js'
@@ -25,6 +29,7 @@ export interface DevWatchOptions {
25
29
  framework: Framework
26
30
  cliOptions: Options
27
31
  packageManager: string
32
+ runDevCommand?: boolean
28
33
  environment: Environment
29
34
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
30
35
  }
@@ -82,6 +87,8 @@ export class DevWatchManager {
82
87
  private tempDir: string | null = null
83
88
  private isBuilding = false
84
89
  private buildCount = 0
90
+ private appDevProcess: ReturnType<typeof spawn> | null = null
91
+ private lastSyncedSourceFiles: Set<string> | null = null
85
92
 
86
93
  constructor(private options: DevWatchOptions) {
87
94
  this.syncer = new FileSyncer()
@@ -110,6 +117,9 @@ export class DevWatchManager {
110
117
  console.log(chalk.bold('dev-watch'))
111
118
  this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`)
112
119
  this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`)
120
+ if (this.options.runDevCommand) {
121
+ this.log.tree('', `app dev server: ${chalk.cyan('enabled')}`)
122
+ }
113
123
  this.log.tree('', 'ready', true)
114
124
 
115
125
  // Setup signal handlers
@@ -118,6 +128,10 @@ export class DevWatchManager {
118
128
 
119
129
  // Start watching
120
130
  this.startWatcher()
131
+
132
+ if (this.options.runDevCommand) {
133
+ this.startAppDevServer()
134
+ }
121
135
  }
122
136
 
123
137
  async stop(): Promise<void> {
@@ -196,33 +210,30 @@ export class DevWatchManager {
196
210
  this.log.section(`build #${buildId}`)
197
211
  const startTime = Date.now()
198
212
 
199
- if (!this.options.frameworkDefinitionInitializers) {
200
- throw new Error(
201
- 'There must be framework initalizers passed to frameworkDefinitionInitializers to use --dev-watch',
202
- )
203
- }
213
+ let refreshedFramework: FrameworkDefinition | null | undefined =
214
+ this.createFrameworkDefinitionFromWatchPath()
204
215
 
205
- const refreshedFrameworks =
206
- this.options.frameworkDefinitionInitializers.map(
207
- (frameworkInitalizer) => frameworkInitalizer(),
208
- )
216
+ if (!refreshedFramework && this.options.frameworkDefinitionInitializers) {
217
+ const refreshedFrameworks =
218
+ this.options.frameworkDefinitionInitializers.map(
219
+ (frameworkInitalizer) => frameworkInitalizer(),
220
+ )
209
221
 
210
- const refreshedFramework = refreshedFrameworks.find(
211
- (f) => f.id === this.options.framework.id,
212
- )
222
+ refreshedFramework = refreshedFrameworks.find(
223
+ (f) => f.id === this.options.framework.id,
224
+ )
225
+ }
213
226
 
214
227
  if (!refreshedFramework) {
215
- throw new Error('Could not identify the framework')
228
+ throw new Error(
229
+ 'Could not refresh framework from watch path or framework initializers',
230
+ )
216
231
  }
217
232
 
218
233
  // Update the chosen addons to use the latest code
219
234
  const chosenAddonIds = this.options.cliOptions.chosenAddOns.map(
220
235
  (m) => m.id,
221
236
  )
222
- const updatedChosenAddons = refreshedFramework.addOns.filter((f) =>
223
- chosenAddonIds.includes(f.id),
224
- )
225
-
226
237
  // Create temp directory for this build using tempy
227
238
  this.tempDir = temporaryDirectory()
228
239
 
@@ -242,22 +253,29 @@ export class DevWatchManager {
242
253
  )
243
254
  }
244
255
 
245
- // Check if package.json was modified
246
- const packageJsonModified = Array.from(changes).some(
247
- (filePath) => path.basename(filePath) === 'package.json',
256
+ const updatedChosenAddons = await finalizeAddOns(
257
+ registeredFramework,
258
+ this.options.cliOptions.mode,
259
+ chosenAddonIds,
248
260
  )
249
261
 
262
+ // Check if package metadata was modified
263
+ const packageMetadataChanged = Array.from(changes).some((filePath) => {
264
+ const normalized = filePath.replace(/\\/g, '/')
265
+ return /(^|\/)package\.json(\.ejs)?$/.test(normalized)
266
+ })
267
+
250
268
  const updatedOptions: Options = {
251
269
  ...this.options.cliOptions,
252
270
  chosenAddOns: updatedChosenAddons,
253
271
  framework: registeredFramework,
254
272
  targetDir: this.tempDir,
255
273
  git: false,
256
- install: packageJsonModified,
274
+ install: packageMetadataChanged,
257
275
  }
258
276
 
259
277
  // Show package installation indicator if needed
260
- if (packageJsonModified) {
278
+ if (packageMetadataChanged) {
261
279
  this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`)
262
280
  }
263
281
 
@@ -269,10 +287,11 @@ export class DevWatchManager {
269
287
  await createApp(silentEnvironment, updatedOptions)
270
288
 
271
289
  // Sync files to target directory
272
- const syncResult = await this.syncer.sync(
273
- this.tempDir,
274
- this.options.targetDir,
275
- )
290
+ const syncResult = await this.syncer.sync(this.tempDir, this.options.targetDir, {
291
+ deleteRemoved: this.lastSyncedSourceFiles !== null,
292
+ previousSourceFiles: this.lastSyncedSourceFiles ?? undefined,
293
+ })
294
+ this.lastSyncedSourceFiles = new Set(syncResult.sourceFiles)
276
295
 
277
296
  // Clean up temp directory after sync is complete
278
297
  try {
@@ -288,7 +307,7 @@ export class DevWatchManager {
288
307
  // Build tree-style summary
289
308
  this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`)
290
309
 
291
- if (packageJsonModified) {
310
+ if (packageMetadataChanged) {
292
311
  this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`)
293
312
  }
294
313
 
@@ -296,19 +315,29 @@ export class DevWatchManager {
296
315
  const noMoreTreeItems =
297
316
  syncResult.updated.length === 0 &&
298
317
  syncResult.created.length === 0 &&
318
+ syncResult.deleted.length === 0 &&
299
319
  syncResult.errors.length === 0
300
320
 
301
321
  if (syncResult.updated.length > 0) {
302
322
  this.log.tree(
303
323
  ' ',
304
324
  `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`,
305
- syncResult.created.length === 0 && syncResult.errors.length === 0,
325
+ syncResult.created.length === 0 &&
326
+ syncResult.deleted.length === 0 &&
327
+ syncResult.errors.length === 0,
306
328
  )
307
329
  }
308
330
  if (syncResult.created.length > 0) {
309
331
  this.log.tree(
310
332
  ' ',
311
333
  `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`,
334
+ syncResult.deleted.length === 0 && syncResult.errors.length === 0,
335
+ )
336
+ }
337
+ if (syncResult.deleted.length > 0) {
338
+ this.log.tree(
339
+ ' ',
340
+ `deleted: ${chalk.green(syncResult.deleted.length + ' file' + (syncResult.deleted.length > 1 ? 's' : ''))}`,
312
341
  syncResult.errors.length === 0,
313
342
  )
314
343
  }
@@ -368,11 +397,19 @@ export class DevWatchManager {
368
397
  // Show created files
369
398
  if (syncResult.created.length > 0) {
370
399
  syncResult.created.forEach((file, index) => {
371
- const isLast = index === syncResult.created.length - 1
400
+ const isLast =
401
+ index === syncResult.created.length - 1 && syncResult.deleted.length === 0
372
402
  this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast)
373
403
  })
374
404
  }
375
405
 
406
+ if (syncResult.deleted.length > 0) {
407
+ syncResult.deleted.forEach((file, index) => {
408
+ const isLast = index === syncResult.deleted.length - 1
409
+ this.log.treeItem(' ', `${chalk.red('-')} ${file}`, isLast)
410
+ })
411
+ }
412
+
376
413
  // Always show errors
377
414
  if (syncResult.errors.length > 0) {
378
415
  console.log() // Add spacing
@@ -393,6 +430,46 @@ export class DevWatchManager {
393
430
  }
394
431
  }
395
432
 
433
+ private createFrameworkDefinitionFromWatchPath(): FrameworkDefinition | null {
434
+ const frameworkRoot = this.options.watchPath
435
+ const projectDirectory = path.join(frameworkRoot, 'project')
436
+ const baseDirectory = path.join(projectDirectory, 'base')
437
+
438
+ if (!fs.existsSync(projectDirectory) || !fs.existsSync(baseDirectory)) {
439
+ return null
440
+ }
441
+
442
+ const addOnDirectoryCandidates = [
443
+ path.join(frameworkRoot, 'add-ons'),
444
+ path.join(frameworkRoot, 'toolchains'),
445
+ path.join(frameworkRoot, 'examples'),
446
+ path.join(frameworkRoot, 'hosts'),
447
+ ]
448
+
449
+ const addOnDirectories = addOnDirectoryCandidates.filter((dir) =>
450
+ fs.existsSync(dir),
451
+ )
452
+
453
+ const addOns =
454
+ addOnDirectories.length > 0 ? scanAddOnDirectories(addOnDirectories) : []
455
+ const { files, basePackageJSON, optionalPackages } = scanProjectDirectory(
456
+ projectDirectory,
457
+ baseDirectory,
458
+ )
459
+
460
+ return {
461
+ id: this.options.framework.id,
462
+ name: this.options.framework.name,
463
+ description: this.options.framework.description,
464
+ version: this.options.framework.version,
465
+ base: files,
466
+ addOns,
467
+ basePackageJSON,
468
+ optionalPackages,
469
+ supportedModes: this.options.framework.supportedModes,
470
+ }
471
+ }
472
+
396
473
  private cleanup(): void {
397
474
  console.log()
398
475
  console.log('Cleaning up...')
@@ -408,9 +485,66 @@ export class DevWatchManager {
408
485
  }
409
486
  }
410
487
 
488
+ if (this.appDevProcess && !this.appDevProcess.killed) {
489
+ this.appDevProcess.kill('SIGTERM')
490
+ this.appDevProcess = null
491
+ }
492
+
411
493
  process.exit(0)
412
494
  }
413
495
 
496
+ private startAppDevServer(): void {
497
+ if (this.appDevProcess) {
498
+ return
499
+ }
500
+
501
+ const { command, args } = this.getDevCommandForPackageManager(
502
+ this.options.packageManager,
503
+ )
504
+
505
+ this.log.section('app dev server')
506
+ this.log.tree(' ', `starting: ${chalk.cyan([command, ...args].join(' '))}`)
507
+
508
+ this.appDevProcess = spawn(command, args, {
509
+ cwd: this.options.targetDir,
510
+ stdio: 'inherit',
511
+ shell: process.platform === 'win32',
512
+ env: process.env,
513
+ })
514
+
515
+ this.appDevProcess.on('exit', (code, signal) => {
516
+ if (signal) {
517
+ this.log.warning(`app dev server exited via signal ${signal}`)
518
+ } else if (code && code !== 0) {
519
+ this.log.warning(`app dev server exited with code ${code}`)
520
+ }
521
+ this.appDevProcess = null
522
+ })
523
+
524
+ this.appDevProcess.on('error', (error) => {
525
+ this.log.error(`Failed to start app dev server: ${error.message}`)
526
+ this.appDevProcess = null
527
+ })
528
+ }
529
+
530
+ private getDevCommandForPackageManager(packageManager: string): {
531
+ command: string
532
+ args: Array<string>
533
+ } {
534
+ switch (packageManager) {
535
+ case 'npm':
536
+ return { command: 'npm', args: ['run', 'dev'] }
537
+ case 'yarn':
538
+ return { command: 'yarn', args: ['dev'] }
539
+ case 'bun':
540
+ return { command: 'bun', args: ['run', 'dev'] }
541
+ case 'deno':
542
+ return { command: 'deno', args: ['task', 'dev'] }
543
+ default:
544
+ return { command: 'pnpm', args: ['dev'] }
545
+ }
546
+ }
547
+
414
548
  private log = {
415
549
  tree: (prefix: string, msg: string, isLast = false) => {
416
550
  const connector = isLast ? '└─' : '├─'
@@ -12,15 +12,28 @@ export interface SyncResult {
12
12
  updated: Array<FileUpdate>
13
13
  skipped: Array<string>
14
14
  created: Array<string>
15
+ deleted: Array<string>
16
+ sourceFiles: Array<string>
15
17
  errors: Array<string>
16
18
  }
17
19
 
20
+ export interface SyncOptions {
21
+ deleteRemoved?: boolean
22
+ previousSourceFiles?: Set<string>
23
+ }
24
+
18
25
  export class FileSyncer {
19
- async sync(sourceDir: string, targetDir: string): Promise<SyncResult> {
26
+ async sync(
27
+ sourceDir: string,
28
+ targetDir: string,
29
+ options?: SyncOptions,
30
+ ): Promise<SyncResult> {
20
31
  const result: SyncResult = {
21
32
  updated: [],
22
33
  skipped: [],
23
34
  created: [],
35
+ deleted: [],
36
+ sourceFiles: [],
24
37
  errors: [],
25
38
  }
26
39
 
@@ -35,6 +48,16 @@ export class FileSyncer {
35
48
  // Walk through source directory and sync files
36
49
  await this.syncDirectory(sourceDir, targetDir, sourceDir, result)
37
50
 
51
+ if (options?.deleteRemoved && options.previousSourceFiles) {
52
+ const currentSourceFileSet = new Set(result.sourceFiles)
53
+ await this.deleteRemovedFiles(
54
+ targetDir,
55
+ options.previousSourceFiles,
56
+ currentSourceFileSet,
57
+ result,
58
+ )
59
+ }
60
+
38
61
  return result
39
62
  }
40
63
 
@@ -72,6 +95,8 @@ export class FileSyncer {
72
95
  continue
73
96
  }
74
97
 
98
+ result.sourceFiles.push(relativePath)
99
+
75
100
  try {
76
101
  const shouldUpdate = await this.shouldUpdateFile(
77
102
  sourcePath,
@@ -202,4 +227,37 @@ export class FileSyncer {
202
227
  const ext = path.extname(name).toLowerCase()
203
228
  return skipExtensions.includes(ext)
204
229
  }
230
+
231
+ private async deleteRemovedFiles(
232
+ targetDir: string,
233
+ previousSourceFiles: Set<string>,
234
+ currentSourceFiles: Set<string>,
235
+ result: SyncResult,
236
+ ): Promise<void> {
237
+ for (const relativePath of previousSourceFiles) {
238
+ if (currentSourceFiles.has(relativePath)) {
239
+ continue
240
+ }
241
+
242
+ const targetPath = path.join(targetDir, relativePath)
243
+
244
+ try {
245
+ if (!fs.existsSync(targetPath)) {
246
+ continue
247
+ }
248
+
249
+ const stats = await fs.promises.stat(targetPath)
250
+ if (!stats.isFile()) {
251
+ continue
252
+ }
253
+
254
+ await fs.promises.unlink(targetPath)
255
+ result.deleted.push(relativePath)
256
+ } catch (error) {
257
+ result.errors.push(
258
+ `${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
259
+ )
260
+ }
261
+ }
262
+ }
205
263
  }
package/src/index.ts CHANGED
@@ -1 +1,21 @@
1
- export { cli } from './cli.js'
1
+ import { pathToFileURL } from 'node:url'
2
+ import {
3
+ createReactFrameworkDefinition,
4
+ createSolidFrameworkDefinition,
5
+ } from '@tanstack/create'
6
+
7
+ import { cli } from './cli.js'
8
+
9
+ export { cli }
10
+
11
+ const entryPath = process.argv[1]
12
+ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
13
+ cli({
14
+ name: 'tanstack',
15
+ appName: 'TanStack',
16
+ frameworkDefinitionInitializers: [
17
+ createReactFrameworkDefinition,
18
+ createSolidFrameworkDefinition,
19
+ ],
20
+ })
21
+ }
package/src/options.ts CHANGED
@@ -41,7 +41,7 @@ export async function promptForCreateOptions(
41
41
  ): Promise<Required<Options> | undefined> {
42
42
  const options = {} as Required<Options>
43
43
 
44
- options.framework = getFrameworkById(cliOptions.framework || 'react-cra')!
44
+ options.framework = getFrameworkById(cliOptions.framework || 'react')!
45
45
 
46
46
  // Validate project name
47
47
  if (cliOptions.projectName) {
@@ -63,8 +63,14 @@ export async function promptForCreateOptions(
63
63
  // Mode is always file-router (TanStack Start)
64
64
  options.mode = 'file-router'
65
65
  const template = cliOptions.template?.toLowerCase().trim()
66
+ const isLegacyTemplate =
67
+ template &&
68
+ ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(
69
+ template,
70
+ )
66
71
  const routerOnly =
67
- !!cliOptions.routerOnly || (template ? template !== 'file-router' : false)
72
+ !!cliOptions.routerOnly ||
73
+ (isLegacyTemplate ? template !== 'file-router' : false)
68
74
 
69
75
  // TypeScript is always enabled with file-router
70
76
  options.typescript = true
package/src/types.ts CHANGED
@@ -13,10 +13,11 @@ export interface CliOptions {
13
13
  mcp?: boolean
14
14
  mcpSse?: boolean
15
15
  starter?: string
16
+ templateId?: string
16
17
  targetDir?: string
17
18
  interactive?: boolean
18
- ui?: boolean
19
19
  devWatch?: string
20
+ runDev?: boolean
20
21
  install?: boolean
21
22
  addOnConfig?: string
22
23
  force?: boolean
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }