Skip to content

Commit 12b82dc

Browse files
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
1 parent a03aca8 commit 12b82dc

12 files changed

Lines changed: 855 additions & 19 deletions

File tree

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()
@@ -13,4 +19,8 @@ cli({
1319
forcedMode: 'file-router',
1420
forcedAddOns: ['start'],
1521
craCompatible: true,
22+
frameworkDefinitionInitializers: [
23+
createReactCraFrameworkDefinitionInitalizer,
24+
createSolidFrameworkDefinitionInitalizer,
25+
],
1626
})

frameworks/react-cra/add-ons/start/assets/src/routes/api.demo-names.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const ServerRoute = createServerFileRoute().methods({
1+
import { createServerFileRoute } from '@tanstack/react-start/server'
2+
3+
export const ServerRoute = createServerFileRoute('/api/demo-names').methods({
24
GET: async ({ request }) => {
35
return new Response(JSON.stringify(['Alice', 'Bob', 'Charlie']), {
46
headers: {

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

packages/cta-cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,17 @@
3737
"@tanstack/cta-engine": "workspace:*",
3838
"@tanstack/cta-ui": "workspace:*",
3939
"chalk": "^5.4.1",
40+
"chokidar": "^3.6.0",
4041
"commander": "^13.1.0",
42+
"diff": "^7.0.0",
4143
"express": "^4.21.2",
4244
"semver": "^7.7.2",
45+
"tempy": "^3.1.0",
4346
"zod": "^3.24.2"
4447
},
4548
"devDependencies": {
4649
"@tanstack/config": "^0.16.2",
50+
"@types/diff": "^5.2.0",
4751
"@types/express": "^5.0.1",
4852
"@types/node": "^22.13.4",
4953
"@types/semver": "^7.7.0",

packages/cta-cli/src/cli.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
createApp,
1414
createSerializedOptions,
1515
getAllAddOns,
16-
getFrameworkById,
1716
getFrameworkByName,
1817
getFrameworks,
1918
initAddOn,
@@ -25,13 +24,18 @@ import { launchUI } from '@tanstack/cta-ui'
2524
import { runMCPServer } from './mcp.js'
2625

2726
import { promptForAddOns, promptForCreateOptions } from './options.js'
28-
import { normalizeOptions } from './command-line.js'
27+
import { normalizeOptions, validateDevWatchOptions } from './command-line.js'
2928

3029
import { createUIEnvironment } from './ui-environment.js'
3130
import { convertTemplateToMode } from './utils.js'
31+
import { DevWatchManager } from './dev-watch.js'
3232

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

3640
// This CLI assumes that all of the registered frameworks have the same set of toolchains, modes, etc.
3741

@@ -44,6 +48,7 @@ export function cli({
4448
defaultFramework,
4549
craCompatible = false,
4650
webBase,
51+
frameworkDefinitionInitializers,
4752
}: {
4853
name: string
4954
appName: string
@@ -53,6 +58,7 @@ export function cli({
5358
defaultFramework?: string
5459
craCompatible?: boolean
5560
webBase?: string
61+
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
5662
}) {
5763
const environment = createUIEnvironment(appName, false)
5864

@@ -280,6 +286,7 @@ Remove your node_modules directory and package lock file and re-install.`,
280286
'initialize this project from a starter URL',
281287
false,
282288
)
289+
.option('--no-install', 'skip installing dependencies')
283290
.option<PackageManager>(
284291
`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
285292
`Explicitly tell the CLI to use this package manager`,
@@ -294,6 +301,10 @@ Remove your node_modules directory and package lock file and re-install.`,
294301
return value as PackageManager
295302
},
296303
)
304+
.option(
305+
'--dev-watch <path>',
306+
'Watch a framework directory for changes and auto-rebuild',
307+
)
297308

298309
if (toolchains.size > 0) {
299310
program.option<string>(
@@ -352,6 +363,67 @@ Remove your node_modules directory and package lock file and re-install.`,
352363
forcedAddOns,
353364
appName,
354365
})
366+
} else if (options.devWatch) {
367+
// Validate dev watch options
368+
const validation = validateDevWatchOptions({ ...options, projectName })
369+
if (!validation.valid) {
370+
console.error(validation.error)
371+
process.exit(1)
372+
}
373+
374+
// Enter dev watch mode
375+
if (!projectName && !options.targetDir) {
376+
console.error(
377+
'Project name/target directory is required for dev watch mode',
378+
)
379+
process.exit(1)
380+
}
381+
382+
if (!options.framework) {
383+
console.error('Failed to detect framework')
384+
process.exit(1)
385+
}
386+
387+
const framework = getFrameworkByName(options.framework)
388+
if (!framework) {
389+
console.error('Failed to detect framework')
390+
process.exit(1)
391+
}
392+
393+
// First, create the app normally using the standard flow
394+
const normalizedOpts = await normalizeOptions(
395+
{
396+
...options,
397+
projectName,
398+
framework: framework.id,
399+
},
400+
defaultMode,
401+
forcedAddOns,
402+
)
403+
404+
if (!normalizedOpts) {
405+
throw new Error('Failed to normalize options')
406+
}
407+
408+
normalizedOpts.targetDir =
409+
options.targetDir || resolve(process.cwd(), projectName)
410+
411+
// Create the initial app
412+
intro(`Creating initial ${appName} app in ${normalizedOpts.targetDir}...`)
413+
await createApp(environment, normalizedOpts)
414+
415+
// Now start the dev watch mode
416+
const manager = new DevWatchManager({
417+
watchPath: options.devWatch,
418+
targetDir: normalizedOpts.targetDir,
419+
framework,
420+
cliOptions: normalizedOpts,
421+
packageManager: normalizedOpts.packageManager,
422+
environment,
423+
frameworkDefinitionInitializers,
424+
})
425+
426+
await manager.start()
355427
} else {
356428
try {
357429
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,
@@ -114,7 +115,57 @@ export async function normalizeOptions(
114115
getPackageManager() ||
115116
DEFAULT_PACKAGE_MANAGER,
116117
git: !!cliOptions.git,
118+
install: cliOptions.install,
117119
chosenAddOns,
118120
starter: starter,
119121
}
120122
}
123+
124+
export function validateDevWatchOptions(cliOptions: CliOptions): {
125+
valid: boolean
126+
error?: string
127+
} {
128+
if (!cliOptions.devWatch) {
129+
return { valid: true }
130+
}
131+
132+
// Validate watch path exists
133+
const watchPath = resolve(process.cwd(), cliOptions.devWatch)
134+
if (!fs.existsSync(watchPath)) {
135+
return {
136+
valid: false,
137+
error: `Watch path does not exist: ${watchPath}`,
138+
}
139+
}
140+
141+
// Validate it's a directory
142+
const stats = fs.statSync(watchPath)
143+
if (!stats.isDirectory()) {
144+
return {
145+
valid: false,
146+
error: `Watch path is not a directory: ${watchPath}`,
147+
}
148+
}
149+
150+
// Ensure target directory is specified
151+
if (!cliOptions.projectName && !cliOptions.targetDir) {
152+
return {
153+
valid: false,
154+
error: 'Project name or target directory is required for dev watch mode',
155+
}
156+
}
157+
158+
// Check for framework structure
159+
const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
160+
const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
161+
const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
162+
163+
if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
164+
return {
165+
valid: false,
166+
error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
167+
}
168+
}
169+
170+
return { valid: true }
171+
}

0 commit comments

Comments
 (0)