@@ -8,43 +8,71 @@ import { runNestCommand } from './run-nest-command';
88import symbols from './symbols' ;
99import { copyPermissionsJSON , deleteSDKFolder , syncAPI } from './sync-api' ;
1010import { addDepsIfNone , copyTsdkConfig , syncFiles } from './sync-files' ;
11- import { measureExecutionTime } from './utils' ;
11+ import { measureExecutionTime , replaceWindowsPath } from './utils' ;
12+ import * as watcher from '@parcel/watcher' ;
13+ import * as path from 'path' ;
14+
15+ // CLI command definitions
16+ // Purpose: Define all CLI commands, their help text, and usage examples
17+ // Each command should have clear, structured documentation
1218
1319const CLI_COMMANDS = {
1420 help : `
15- Usage
16- $ tsdk
17-
18- Options
19- --help Help
20- --init Initialize \`tsdk\` config file
21- --init --no-zod Don't add \`zod\` to deps
22- --sync Sync files and generate api
23- --sync --no-vscode Skip copy \`.vscode/\`
24- --sync --no-zod Don't add \`zod\` to deps
25- --sync --no-overwrite Default is overwrite with template files(no overwrite for create custom files)
26- --nest Run nest command, only support build
27- --openapi Translate openapi.yaml or openapi.json to openapi.apiconf.ts
28- --version The version info
29-
30- Examples
21+ Usage:
22+ $ tsdk [command] [options]
23+
24+ Commands:
25+ --help Show this help message
26+ --version Show version information
27+ --init Initialize tsdk configuration file
28+ --sync Sync files and generate API
29+ --watch Watch mode for continuous sync
30+ --nest <command> Run NestJS build commands
31+ --openapi <file> Convert OpenAPI spec to apiconf.ts (Better use with -o <output dir>)
32+
33+ Options:
34+ --build Run tsc build after sync (with --sync)
35+ --no-zod Skip adding zod to dependencies (with --init or --sync)
36+ --no-vscode Skip copying .vscode/ directory (with --sync)
37+ --no-overwrite Preserve existing files, only create new ones (with --sync)
38+
39+ Examples:
3140 $ tsdk --version
3241 $ tsdk --help
3342 $ tsdk --init
43+ $ tsdk --init --no-zod
3444 $ tsdk --sync
45+ $ tsdk --sync --build
3546 $ tsdk --sync --no-overwrite
36- $
47+ $ tsdk --sync --no-vscode --no-zod
48+ $ tsdk --watch
3749 $ tsdk --nest build
38- $ tsdk --nest build [ name] [name]
50+ $ tsdk --nest build <app- name>
3951 $ tsdk --nest build all
40- $
41- $ tsdk --openapi openapi.yaml
42- $ tsdk --openapi openapi.json
52+ $ tsdk --openapi openapi.yaml -o <ouput-dir>
53+ $ tsdk --openapi openapi.json -o <ouput-dir>
4354` ,
44- init : `init \`tsdk\` config file` ,
45- sync : `generate api` ,
46- nest : `@nestjs/cli enchance` ,
47- } ;
55+
56+ // Short descriptions for programmatic use
57+ init : 'Initialize tsdk configuration file' ,
58+ sync : 'Sync files and generate API code from configuration' ,
59+ watch : 'Watch for changes and auto-sync' ,
60+ nest : 'Run NestJS CLI commands (currently supports: build)' ,
61+ openapi : 'Convert OpenAPI specification (YAML or JSON) to TypeScript API configuration' ,
62+ version : 'Display tsdk version information' ,
63+ } as const ;
64+
65+ // Validate that a command exists
66+ function isValidCommand ( cmd : string ) : cmd is keyof typeof CLI_COMMANDS {
67+ return cmd in CLI_COMMANDS ;
68+ }
69+
70+ // Get command description
71+ function getCommandDescription ( cmd : keyof typeof CLI_COMMANDS ) : string {
72+ return CLI_COMMANDS [ cmd ] ;
73+ }
74+
75+ export { CLI_COMMANDS , isValidCommand , getCommandDescription } ;
4876
4977const VALID_PROJECT_MSG = `Please run \`tsdk\` in a valid TypeScript project! Check: https://tsdk.dev/docs/start-a-typescript-project` ;
5078
@@ -73,18 +101,25 @@ run();
73101/**
74102 * Handles sync command with parallelization where possible
75103 * @param noOverwrite Whether to use no-overwrite mode
104+ * @param needBuild Run build
76105 */
77- async function handleSyncCommand ( noOverwrite : boolean ) : Promise < void > {
106+ async function handleSyncCommand (
107+ noOverwrite : boolean ,
108+ needBuild = false ,
109+ prettier = true
110+ ) : Promise < void > {
78111 try {
79- await measureExecutionTime ( ' Delete SDK folder' , ( ) => deleteSDKFolder ( ) ) ;
112+ await measureExecutionTime ( ` Delete SDK ${ config . baseDir } folder` , ( ) => deleteSDKFolder ( ) ) ;
80113 await measureExecutionTime ( 'Add dependencies if none' , ( ) => addDepsIfNone ( ) ) ;
81114
82115 const result = await measureExecutionTime ( 'Sync files' , ( ) => syncFiles ( noOverwrite ) ) ;
83116
84117 await measureExecutionTime ( 'Generating API' , ( ) =>
85118 syncAPI ( result ?. apiconfs || [ ] , result ?. types || [ ] )
86119 ) ;
87- await measureExecutionTime ( 'Build SDK' , ( ) => buildSDK ( true ) ) ;
120+ if ( needBuild ) {
121+ await measureExecutionTime ( 'Build SDK' , ( ) => buildSDK ( true ) ) ;
122+ }
88123
89124 const removeFieldsValue = config . removeFields ?? [ 'needAuth' ] ;
90125 // Execute these tasks in parallel
@@ -97,15 +132,150 @@ async function handleSyncCommand(noOverwrite: boolean): Promise<void> {
97132 ] ) ;
98133 } ) ;
99134
100- const prettierSuccess = await measureExecutionTime ( 'Run Prettier' , ( ) => runPrettier ( ) ) ;
101- if ( prettierSuccess ) console . log ( `${ symbols . success } Prettier files\n` ) ;
135+ if ( prettier ) {
136+ const prettierSuccess = await measureExecutionTime ( 'Run Prettier' , ( ) => runPrettier ( ) ) ;
137+ if ( prettierSuccess ) console . log ( `${ symbols . success } Prettier files\n` ) ;
138+ }
102139 } catch ( error ) {
103140 console . error ( `\n${ symbols . error } Sync command failed:` ) ;
104141 console . error ( error ) ;
105142 process . exit ( 1 ) ;
106143 }
107144}
108145
146+ /**
147+ * Watch mode implementation using @parcel/watcher
148+ * Monitors apiconf files and triggers sync on changes
149+ * @param noOverwrite Whether to use no-overwrite mode
150+ * @param needBuild Run build after each sync
151+ */
152+ async function handleWatchCommand ( noOverwrite : boolean , needBuild = false ) : Promise < void > {
153+ console . log ( `${ symbols . info } Starting watch mode...` ) ;
154+
155+ // Run initial sync
156+ console . log ( `${ symbols . info } Running initial sync...\n` ) ;
157+ await handleSyncCommand ( noOverwrite , needBuild , false ) ;
158+ const pattern = path . join ( ...config . baseDir . split ( '/' ) ) ;
159+ // Determine watch directories from config
160+ const watchDirs : string [ ] = [ pattern ] ;
161+
162+ // Add sharedDirs
163+ if ( config . sharedDirs && config . sharedDirs . length > 0 ) {
164+ config . sharedDirs . forEach ( ( dir ) => {
165+ const absoluteDir = path . isAbsolute ( dir ) ? dir : path . join ( process . cwd ( ) , dir ) ;
166+ watchDirs . push ( absoluteDir ) ;
167+ } ) ;
168+ }
169+
170+ console . log ( `\n${ symbols . info } Watching for changes in:` ) ;
171+ watchDirs . forEach ( ( dir ) => console . log ( ` - ${ dir } ` ) ) ;
172+ console . log ( `${ symbols . info } Press Ctrl+C to stop\n` ) ;
173+
174+ // Build file extension patterns from config
175+ const { apiconfExt, entityExt, shareExt } = config ;
176+
177+ // Track last sync time to debounce rapid changes
178+ let lastSyncTime = Date . now ( ) ;
179+ let syncTimeout : NodeJS . Timeout | null = null ;
180+ const DEBOUNCE_MS = 500 ;
181+
182+ /**
183+ * Check if file matches watched extensions
184+ * Matches patterns like: *.apiconf.ts, *.entity.ts, *.shared.ts
185+ * shareExt uses pattern: *.{shareExt}.* (e.g., *.shared.ts, *.shared.json)
186+ */
187+ function isRelevantFile ( filePath : string ) : boolean {
188+ const fileName = path . basename ( filePath ) ;
189+
190+ // Check apiconf pattern: *.apiconf.ts
191+ const apiconfPattern = `.${ apiconfExt } .ts` ;
192+ if ( fileName . endsWith ( apiconfPattern ) ) return true ;
193+
194+ // Check entity pattern: *.entity.ts
195+ const entityPattern = `.${ entityExt } .ts` ;
196+ if ( fileName . endsWith ( entityPattern ) ) return true ;
197+
198+ // Check shareExt pattern: *.{shareExt}.{ext}
199+ // Match files like: *.shared.ts, *.shared.json, *.shared.jpg
200+ // Split on last dot to get the final extension
201+ const parts = fileName . split ( '.' ) ;
202+ if ( parts . length >= 3 ) {
203+ // Check if second-to-last part matches shareExt
204+ const secondToLast = parts [ parts . length - 2 ] ;
205+ if ( secondToLast === shareExt ) return true ;
206+ }
207+
208+ return false ;
209+ }
210+
211+ try {
212+ // Subscribe to all watch directories
213+ const subscriptions = await Promise . all (
214+ watchDirs . map ( async ( watchDir ) => {
215+ return watcher . subscribe ( watchDir , async ( err , events ) => {
216+ if ( err ) {
217+ console . error ( `\n${ symbols . error } Watch error in ${ watchDir } :` , err ) ;
218+ return ;
219+ }
220+
221+ // Filter for relevant file changes based on configured extensions
222+ const relevantChanges = events . filter ( ( event ) => isRelevantFile ( event . path ) ) ;
223+
224+ if ( relevantChanges . length === 0 ) return ;
225+
226+ // Log detected changes
227+ console . log ( `\n${ symbols . info } Detected changes:` ) ;
228+ relevantChanges . forEach ( ( event ) => {
229+ const relativePath = path . relative ( process . cwd ( ) , event . path ) ;
230+ console . log ( ` ${ event . type } : ${ relativePath } ` ) ;
231+ } ) ;
232+
233+ // Debounce: wait for changes to settle before syncing
234+ if ( syncTimeout ) clearTimeout ( syncTimeout ) ;
235+
236+ syncTimeout = setTimeout ( async ( ) => {
237+ const now = Date . now ( ) ;
238+ const timeSinceLastSync = now - lastSyncTime ;
239+
240+ if ( timeSinceLastSync < DEBOUNCE_MS ) return ;
241+
242+ lastSyncTime = now ;
243+ console . log ( `\n${ symbols . info } Syncing changes...\n` ) ;
244+
245+ try {
246+ await handleSyncCommand ( noOverwrite , needBuild , false ) ;
247+ console . log (
248+ `\n${ symbols . success } Sync complete in ${ Date . now ( ) - lastSyncTime } ms. Watching for changes...\n`
249+ ) ;
250+ } catch ( error ) {
251+ console . error ( `\n${ symbols . error } Sync failed:` , error ) ;
252+ console . log ( `\n${ symbols . info } Continuing to watch for changes...\n` ) ;
253+ }
254+ } , DEBOUNCE_MS ) ;
255+ } ) ;
256+ } )
257+ ) ;
258+
259+ // Handle graceful shutdown
260+ const cleanup = async ( ) => {
261+ console . log ( `\n\n${ symbols . info } Shutting down watch mode...` ) ;
262+ if ( syncTimeout ) clearTimeout ( syncTimeout ) ;
263+ await Promise . all ( subscriptions . map ( ( sub ) => sub . unsubscribe ( ) ) ) ;
264+ console . log ( `${ symbols . success } Watch mode stopped\n` ) ;
265+ process . exit ( 0 ) ;
266+ } ;
267+
268+ process . on ( 'SIGINT' , cleanup ) ;
269+ process . on ( 'SIGTERM' , cleanup ) ;
270+
271+ // Keep process alive
272+ await new Promise ( ( ) => { } ) ;
273+ } catch ( error ) {
274+ console . error ( `\n${ symbols . error } Failed to start watch mode:` , error ) ;
275+ process . exit ( 1 ) ;
276+ }
277+ }
278+
109279/**
110280 * Handles CLI commands
111281 * @param params Command line parameters
@@ -143,7 +313,15 @@ async function handleCommand(params: string[]): Promise<void> {
143313
144314 case '--sync' : {
145315 const noOverwrite = params . includes ( '--no-overwrite' ) ;
146- await handleSyncCommand ( noOverwrite ) ;
316+ const withBuild = params . includes ( '--build' ) ;
317+ await handleSyncCommand ( noOverwrite , withBuild ) ;
318+ break ;
319+ }
320+
321+ case '--watch' : {
322+ const noOverwrite = params . includes ( '--no-overwrite' ) ;
323+ const withBuild = params . includes ( '--build' ) ;
324+ await handleWatchCommand ( noOverwrite , withBuild ) ;
147325 break ;
148326 }
149327
0 commit comments