1- import { invariant } from '@zenstackhq/common-helpers' ;
2- import { isPlugin , LiteralExpr , Plugin , type Model } from '@zenstackhq/language/ast' ;
1+ import { invariant , singleDebounce } from '@zenstackhq/common-helpers' ;
2+ import { ZModelLanguageMetaData } from '@zenstackhq/language' ;
3+ import { type AbstractDeclaration , isPlugin , LiteralExpr , Plugin , type Model } from '@zenstackhq/language/ast' ;
34import { getLiteral , getLiteralArray } from '@zenstackhq/language/utils' ;
45import { type CliPlugin } from '@zenstackhq/sdk' ;
56import colors from 'colors' ;
67import { createJiti } from 'jiti' ;
78import fs from 'node:fs' ;
89import path from 'node:path' ;
910import { pathToFileURL } from 'node:url' ;
11+ import { watch } from 'chokidar' ;
1012import ora , { type Ora } from 'ora' ;
1113import { CliError } from '../cli-error' ;
1214import * as corePlugins from '../plugins' ;
@@ -16,6 +18,7 @@ type Options = {
1618 schema ?: string ;
1719 output ?: string ;
1820 silent : boolean ;
21+ watch : boolean ;
1922 lite : boolean ;
2023 liteOnly : boolean ;
2124} ;
@@ -24,6 +27,96 @@ type Options = {
2427 * CLI action for generating code from schema
2528 */
2629export async function run ( options : Options ) {
30+ const model = await pureGenerate ( options , false ) ;
31+
32+ if ( options . watch ) {
33+ const logsEnabled = ! options . silent ;
34+
35+ if ( logsEnabled ) {
36+ console . log ( colors . green ( `\nEnabled watch mode!` ) ) ;
37+ }
38+
39+ const schemaExtensions = ZModelLanguageMetaData . fileExtensions ;
40+
41+ // Get real models file path (cuz its merged into single document -> we need use cst nodes)
42+ const getRootModelWatchPaths = ( model : Model ) => new Set < string > (
43+ (
44+ model . declarations . filter (
45+ ( v ) =>
46+ v . $cstNode ?. parent ?. element . $type === 'Model' &&
47+ ! ! v . $cstNode . parent . element . $document ?. uri ?. fsPath ,
48+ ) as AbstractDeclaration [ ]
49+ ) . map ( ( v ) => v . $cstNode ! . parent ! . element . $document ! . uri ! . fsPath ) ,
50+ ) ;
51+
52+ const watchedPaths = getRootModelWatchPaths ( model ) ;
53+
54+ if ( logsEnabled ) {
55+ const logPaths = [ ...watchedPaths ] . map ( ( at ) => `- ${ at } ` ) . join ( '\n' ) ;
56+ console . log ( `Watched file paths:\n${ logPaths } ` ) ;
57+ }
58+
59+ const watcher = watch ( [ ...watchedPaths ] , {
60+ alwaysStat : false ,
61+ ignoreInitial : true ,
62+ ignorePermissionErrors : true ,
63+ ignored : ( at ) => ! schemaExtensions . some ( ( ext ) => at . endsWith ( ext ) ) ,
64+ } ) ;
65+
66+ // prevent save multiple files and run multiple times
67+ const reGenerateSchema = singleDebounce ( async ( ) => {
68+ if ( logsEnabled ) {
69+ console . log ( 'Got changes, run generation!' ) ;
70+ }
71+
72+ try {
73+ const newModel = await pureGenerate ( options , true ) ;
74+ const allModelsPaths = getRootModelWatchPaths ( newModel ) ;
75+ const newModelPaths = [ ...allModelsPaths ] . filter ( ( at ) => ! watchedPaths . has ( at ) ) ;
76+ const removeModelPaths = [ ...watchedPaths ] . filter ( ( at ) => ! allModelsPaths . has ( at ) ) ;
77+
78+ if ( newModelPaths . length ) {
79+ if ( logsEnabled ) {
80+ const logPaths = newModelPaths . map ( ( at ) => `- ${ at } ` ) . join ( '\n' ) ;
81+ console . log ( `Added file(s) to watch:\n${ logPaths } ` ) ;
82+ }
83+
84+ newModelPaths . forEach ( ( at ) => watchedPaths . add ( at ) ) ;
85+ watcher . add ( newModelPaths ) ;
86+ }
87+
88+ if ( removeModelPaths . length ) {
89+ if ( logsEnabled ) {
90+ const logPaths = removeModelPaths . map ( ( at ) => `- ${ at } ` ) . join ( '\n' ) ;
91+ console . log ( `Removed file(s) from watch:\n${ logPaths } ` ) ;
92+ }
93+
94+ removeModelPaths . forEach ( ( at ) => watchedPaths . delete ( at ) ) ;
95+ watcher . unwatch ( removeModelPaths ) ;
96+ }
97+ } catch ( e ) {
98+ console . error ( e ) ;
99+ }
100+ } , 500 , true ) ;
101+
102+ watcher . on ( 'unlink' , ( pathAt ) => {
103+ if ( logsEnabled ) {
104+ console . log ( `Removed file from watch: ${ pathAt } ` ) ;
105+ }
106+
107+ watchedPaths . delete ( pathAt ) ;
108+ watcher . unwatch ( pathAt ) ;
109+
110+ reGenerateSchema ( ) ;
111+ } ) ;
112+
113+ watcher . on ( 'change' , ( ) => {
114+ reGenerateSchema ( ) ;
115+ } ) ;
116+ }
117+ }
118+
119+ async function pureGenerate ( options : Options , fromWatch : boolean ) {
27120 const start = Date . now ( ) ;
28121
29122 const schemaFile = getSchemaFile ( options . schema ) ;
@@ -35,7 +128,9 @@ export async function run(options: Options) {
35128
36129 if ( ! options . silent ) {
37130 console . log ( colors . green ( `Generation completed successfully in ${ Date . now ( ) - start } ms.\n` ) ) ;
38- console . log ( `You can now create a ZenStack client with it.
131+
132+ if ( ! fromWatch ) {
133+ console . log ( `You can now create a ZenStack client with it.
39134
40135\`\`\`ts
41136import { ZenStackClient } from '@zenstackhq/orm';
@@ -47,7 +142,10 @@ const client = new ZenStackClient(schema, {
47142\`\`\`
48143
49144Check documentation: https://zenstack.dev/docs/` ) ;
145+ }
50146 }
147+
148+ return model ;
51149}
52150
53151function getOutputPath ( options : Options , schemaFile : string ) {
0 commit comments