Skip to content

Commit 5641cb1

Browse files
feat: add dev watch command for framework development (#141)
* feat: add dev watch command for framework development Addition of a --dev-watch cli arg so that when working on add-ons you can continually scaffold an app when changes are detected - add --dev-watch cli arg e.g. --dev-watch ./frameworks/react-cra - add --no-install cli arg - skips install deps. Needed so we aren't installing deps when package.json doesn't change in --dev-watch * feat: improve dev watch command output with tree-style formatting - Replace emoji-based logging with cleaner tree-style output - Add visual hierarchy for build progress and file changes - Show inline diffs with proper indentation - Reduce visual noise while maintaining all important information - Create silent environment for initial app creation to avoid duplicate output * docs: add --dev-watch command documentation to CONTRIBUTING.md - Add comprehensive guide for using the --dev-watch feature - Include example workflow with real command examples - Document auto-rebuild and live reload capabilities - Update pnpm-lock.yaml after merge --------- Co-authored-by: Jack Herrington <[email protected]>
1 parent 7647683 commit 5641cb1

File tree

12 files changed

+2179
-87
lines changed

12 files changed

+2179
-87
lines changed

CONTRIBUTING.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,38 @@ npm_config_user_agent=pnpm node ../create-tsrouter-app/cli/create-start-app/dist
4040

4141
If you want to specify a package manager.
4242

43+
# Framework Development with --dev-watch
44+
45+
The `--dev-watch` command provides real-time feedback while developing frameworks, add-ons, and starters. It watches for changes in your framework files and automatically rebuilds them.
46+
47+
## Using --dev-watch
48+
49+
To start developing a framework with live rebuilding:
50+
51+
```bash
52+
node [root of the monorepo]/cli/create-tsrouter-app/dist/index.js --dev-watch ./frameworks/react-cra test-app --template typescript --package-manager bun --tailwind --add-ons shadcn
53+
```
54+
55+
This command will:
56+
57+
- Watch the selected folder for changes (the folder with the add-ons in it)
58+
- Automatically rebuild your app / install packages in the target folder when changes are detected (in this case it will install the shadcn addon)
59+
- Show build output, diffs detected and any errors in real-time
60+
61+
## Example Workflow
62+
63+
1. Start the dev watch mode:
64+
65+
```bash
66+
pnpm dev # Build in watch mode
67+
rm -rf test-app && node cli/create-tsrouter-app/dist/index.js --dev-watch ./frameworks/react-cra test-app --template typescript --package-manager bun --tailwind --add-ons shadcn
68+
cd my-test-app && pnpm run dev # run the tsrouter vite app
69+
```
70+
71+
2. Select the framework you want to work on from the displayed list
72+
73+
3. Make changes to the add-ons - they will be automatically rebuilt and your vite app will reflect the changes
74+
4375
# Testing Add-ons and Starters
4476

4577
Create the add-on or starter using the CLI. Then serve it locally from the project directory using `npx static-server`.

cli/create-start-app/src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
#!/usr/bin/env node
22
import { cli } from '@tanstack/cta-cli'
33

4-
import { register as registerReactCra } from '@tanstack/cta-framework-react-cra'
5-
import { register as registerSolid } from '@tanstack/cta-framework-solid'
4+
import {
5+
createFrameworkDefinition as createReactCraFrameworkDefinitionInitalizer,
6+
register as registerReactCra,
7+
} from '@tanstack/cta-framework-react-cra'
8+
import {
9+
createFrameworkDefinition as createSolidFrameworkDefinitionInitalizer,
10+
register as registerSolid,
11+
} from '@tanstack/cta-framework-solid'
612

713
registerReactCra()
814
registerSolid()
@@ -15,4 +21,8 @@ cli({
1521
showDeploymentOptions: true,
1622
forcedDeployment: 'nitro',
1723
craCompatible: true,
24+
frameworkDefinitionInitializers: [
25+
createReactCraFrameworkDefinitionInitalizer,
26+
createSolidFrameworkDefinitionInitalizer,
27+
],
1828
})

frameworks/react-cra/add-ons/tRPC/assets/src/routes/api.trpc.$.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createServerFileRoute } from '@tanstack/react-start/server'
12
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
23
import { trpcRouter } from '@/integrations/trpc/router'
34
import { createFileRoute } from '@tanstack/react-router'

packages/cta-cli/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@
3838
"@tanstack/cta-engine": "workspace:*",
3939
"@tanstack/cta-ui": "workspace:*",
4040
"chalk": "^5.4.1",
41+
"chokidar": "^3.6.0",
4142
"commander": "^13.1.0",
43+
"diff": "^7.0.0",
4244
"express": "^4.21.2",
4345
"semver": "^7.7.2",
46+
"tempy": "^3.1.0",
4447
"validate-npm-package-name": "^7.0.0",
4548
"zod": "^3.24.2"
4649
},
4750
"devDependencies": {
51+
"@tanstack/config": "^0.16.2",
52+
"@types/diff": "^5.2.0",
4853
"@types/express": "^5.0.1",
4954
"@types/node": "^22.13.4",
5055
"@types/semver": "^7.7.0",

packages/cta-cli/src/cli.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ import { launchUI } from '@tanstack/cta-ui'
2424
import { runMCPServer } from './mcp.js'
2525

2626
import { promptForAddOns, promptForCreateOptions } from './options.js'
27-
import { normalizeOptions } from './command-line.js'
27+
import { normalizeOptions, validateDevWatchOptions } from './command-line.js'
2828

2929
import { createUIEnvironment } from './ui-environment.js'
3030
import { convertTemplateToMode } from './utils.js'
31+
import { DevWatchManager } from './dev-watch.js'
3132

3233
import type { CliOptions, TemplateOptions } from './types.js'
33-
import type { Options, PackageManager } from '@tanstack/cta-engine'
34+
import type {
35+
FrameworkDefinition,
36+
Options,
37+
PackageManager,
38+
} from '@tanstack/cta-engine'
3439

3540
// This CLI assumes that all of the registered frameworks have the same set of toolchains, deployments, modes, etc.
3641

@@ -44,6 +49,7 @@ export function cli({
4449
defaultFramework,
4550
craCompatible = false,
4651
webBase,
52+
frameworkDefinitionInitializers,
4753
showDeploymentOptions = false,
4854
}: {
4955
name: string
@@ -55,6 +61,7 @@ export function cli({
5561
defaultFramework?: string
5662
craCompatible?: boolean
5763
webBase?: string
64+
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
5865
showDeploymentOptions?: boolean
5966
}) {
6067
const environment = createUIEnvironment(appName, false)
@@ -293,6 +300,7 @@ Remove your node_modules directory and package lock file and re-install.`,
293300
'initialize this project from a starter URL',
294301
false,
295302
)
303+
.option('--no-install', 'skip installing dependencies')
296304
.option<PackageManager>(
297305
`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
298306
`Explicitly tell the CLI to use this package manager`,
@@ -307,6 +315,10 @@ Remove your node_modules directory and package lock file and re-install.`,
307315
return value as PackageManager
308316
},
309317
)
318+
.option(
319+
'--dev-watch <path>',
320+
'Watch a framework directory for changes and auto-rebuild',
321+
)
310322

311323
if (deployments.size > 0) {
312324
program.option<string>(
@@ -462,6 +474,73 @@ Remove your node_modules directory and package lock file and re-install.`,
462474
forcedAddOns,
463475
appName,
464476
})
477+
} else if (options.devWatch) {
478+
// Validate dev watch options
479+
const validation = validateDevWatchOptions({ ...options, projectName })
480+
if (!validation.valid) {
481+
console.error(validation.error)
482+
process.exit(1)
483+
}
484+
485+
// Enter dev watch mode
486+
if (!projectName && !options.targetDir) {
487+
console.error(
488+
'Project name/target directory is required for dev watch mode',
489+
)
490+
process.exit(1)
491+
}
492+
493+
if (!options.framework) {
494+
console.error('Failed to detect framework')
495+
process.exit(1)
496+
}
497+
498+
const framework = getFrameworkByName(options.framework)
499+
if (!framework) {
500+
console.error('Failed to detect framework')
501+
process.exit(1)
502+
}
503+
504+
// First, create the app normally using the standard flow
505+
const normalizedOpts = await normalizeOptions(
506+
{
507+
...options,
508+
projectName,
509+
framework: framework.id,
510+
},
511+
defaultMode,
512+
forcedAddOns,
513+
)
514+
515+
if (!normalizedOpts) {
516+
throw new Error('Failed to normalize options')
517+
}
518+
519+
normalizedOpts.targetDir =
520+
options.targetDir || resolve(process.cwd(), projectName)
521+
522+
// Create the initial app with minimal output for dev watch mode
523+
console.log(chalk.bold('\ndev-watch'))
524+
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
525+
if (normalizedOpts.install !== false) {
526+
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
527+
}
528+
const silentEnvironment = createUIEnvironment(appName, true)
529+
await createApp(silentEnvironment, normalizedOpts)
530+
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
531+
532+
// Now start the dev watch mode
533+
const manager = new DevWatchManager({
534+
watchPath: options.devWatch,
535+
targetDir: normalizedOpts.targetDir,
536+
framework,
537+
cliOptions: normalizedOpts,
538+
packageManager: normalizedOpts.packageManager,
539+
environment,
540+
frameworkDefinitionInitializers,
541+
})
542+
543+
await manager.start()
465544
} else {
466545
try {
467546
const cliOptions = {

packages/cta-cli/src/command-line.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { resolve } from 'node:path'
2+
import fs from 'node:fs'
23

34
import {
45
DEFAULT_PACKAGE_MANAGER,
@@ -158,6 +159,7 @@ export async function normalizeOptions(
158159
getPackageManager() ||
159160
DEFAULT_PACKAGE_MANAGER,
160161
git: !!cliOptions.git,
162+
install: cliOptions.install,
161163
chosenAddOns,
162164
addOnOptions: {
163165
...populateAddOnOptionsDefaults(chosenAddOns),
@@ -166,3 +168,52 @@ export async function normalizeOptions(
166168
starter: starter,
167169
}
168170
}
171+
172+
export function validateDevWatchOptions(cliOptions: CliOptions): {
173+
valid: boolean
174+
error?: string
175+
} {
176+
if (!cliOptions.devWatch) {
177+
return { valid: true }
178+
}
179+
180+
// Validate watch path exists
181+
const watchPath = resolve(process.cwd(), cliOptions.devWatch)
182+
if (!fs.existsSync(watchPath)) {
183+
return {
184+
valid: false,
185+
error: `Watch path does not exist: ${watchPath}`,
186+
}
187+
}
188+
189+
// Validate it's a directory
190+
const stats = fs.statSync(watchPath)
191+
if (!stats.isDirectory()) {
192+
return {
193+
valid: false,
194+
error: `Watch path is not a directory: ${watchPath}`,
195+
}
196+
}
197+
198+
// Ensure target directory is specified
199+
if (!cliOptions.projectName && !cliOptions.targetDir) {
200+
return {
201+
valid: false,
202+
error: 'Project name or target directory is required for dev watch mode',
203+
}
204+
}
205+
206+
// Check for framework structure
207+
const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
208+
const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
209+
const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
210+
211+
if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
212+
return {
213+
valid: false,
214+
error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
215+
}
216+
}
217+
218+
return { valid: true }
219+
}

0 commit comments

Comments
 (0)