Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 8e57da0

Browse files
committed
feat(cli): implement watch mode for generate
1 parent 97ec4af commit 8e57da0

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@zenstackhq/common-helpers": "workspace:*",
3636
"@zenstackhq/language": "workspace:*",
3737
"@zenstackhq/sdk": "workspace:*",
38+
"chokidar": "^5.0.0",
3839
"colors": "1.4.0",
3940
"commander": "^8.3.0",
4041
"execa": "^9.6.0",

packages/cli/src/actions/generate.ts

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
2+
import { ZModelLanguageMetaData } from '@zenstackhq/language';
3+
import { isPlugin, isDataModel, type DataModel, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
34
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
45
import { type CliPlugin } from '@zenstackhq/sdk';
56
import colors from 'colors';
@@ -16,6 +17,7 @@ type Options = {
1617
schema?: string;
1718
output?: string;
1819
silent: boolean;
20+
watch: boolean;
1921
lite: boolean;
2022
liteOnly: boolean;
2123
};
@@ -24,6 +26,92 @@ type Options = {
2426
* CLI action for generating code from schema
2527
*/
2628
export async function run(options: Options) {
29+
const model = await pureGenerate(options, false);
30+
31+
if (options.watch) {
32+
const logsEnabled = !options.silent;
33+
34+
if (logsEnabled) {
35+
console.log(colors.green(`\nEnable watch mode!`));
36+
}
37+
38+
const schemaExtensions = ZModelLanguageMetaData.fileExtensions.join(', ');
39+
40+
// Get real models file path (cuz its merged into single document -> we need use cst nodes)
41+
const getModelAllPaths = (model: Model) => new Set(
42+
(
43+
model.declarations.filter(
44+
(v) =>
45+
isDataModel(v) &&
46+
v.$cstNode?.parent?.element.$type === 'Model' &&
47+
!!v.$cstNode.parent.element.$document?.uri?.fsPath,
48+
) as DataModel[]
49+
).map((v) => v.$cstNode!.parent!.element.$document!.uri!.fsPath),
50+
);
51+
52+
const { watch } = await import('chokidar');
53+
54+
const watchedPaths = getModelAllPaths(model);
55+
let reGenerateSchemaTimeout: ReturnType<typeof setTimeout> | undefined;
56+
57+
if (logsEnabled) {
58+
const logPaths = [...watchedPaths].map((at) => `- ${at}`).join('\n');
59+
console.log(`Watched file paths:\n${logPaths}`);
60+
}
61+
62+
const watcher = watch([...watchedPaths], {
63+
alwaysStat: false,
64+
ignoreInitial: true,
65+
ignorePermissionErrors: true,
66+
ignored: (at) => !schemaExtensions.includes(path.extname(at)),
67+
});
68+
69+
const reGenerateSchema = () => {
70+
clearTimeout(reGenerateSchemaTimeout);
71+
72+
// prevent save multiple files and run multiple times
73+
reGenerateSchemaTimeout = setTimeout(async () => {
74+
if (logsEnabled) {
75+
console.log('Got changes, run generation!');
76+
}
77+
78+
try {
79+
const newModel = await pureGenerate(options, true);
80+
const allModelsPaths = getModelAllPaths(newModel);
81+
const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at));
82+
83+
if (newModelPaths.length) {
84+
if (logsEnabled) {
85+
const logPaths = [...newModelPaths].map((at) => `- ${at}`).join('\n');
86+
console.log(`Add file(s) to watch:\n${logPaths}`);
87+
}
88+
89+
watcher.add(newModelPaths);
90+
}
91+
} catch (e) {
92+
console.error(e);
93+
}
94+
}, 500);
95+
};
96+
97+
watcher.on('unlink', (pathAt) => {
98+
if (logsEnabled) {
99+
console.log(`Remove file from watch: ${pathAt}`);
100+
}
101+
102+
watcher.unwatch(pathAt);
103+
watchedPaths.delete(pathAt);
104+
105+
reGenerateSchema();
106+
});
107+
108+
watcher.on('change', () => {
109+
reGenerateSchema();
110+
});
111+
}
112+
}
113+
114+
async function pureGenerate(options: Options, fromWatch: boolean) {
27115
const start = Date.now();
28116

29117
const schemaFile = getSchemaFile(options.schema);
@@ -35,7 +123,9 @@ export async function run(options: Options) {
35123

36124
if (!options.silent) {
37125
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.
126+
127+
if (!fromWatch) {
128+
console.log(`You can now create a ZenStack client with it.
39129
40130
\`\`\`ts
41131
import { ZenStackClient } from '@zenstackhq/orm';
@@ -47,7 +137,10 @@ const client = new ZenStackClient(schema, {
47137
\`\`\`
48138
49139
Check documentation: https://zenstack.dev/docs/`);
140+
}
50141
}
142+
143+
return model;
51144
}
52145

53146
function getOutputPath(options: Options, schemaFile: string) {

packages/cli/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function createProgram() {
6868
.addOption(schemaOption)
6969
.addOption(noVersionCheckOption)
7070
.addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
71+
.addOption(new Option('-w, --watch', 'enable watch mode').default(false))
7172
.addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false))
7273
.addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false))
7374
.addOption(new Option('--silent', 'suppress all output except errors').default(false))
@@ -220,6 +221,11 @@ async function main() {
220221
}
221222
}
222223

224+
if (program.args.includes('generate') && (program.args.includes('-w') || program.args.includes('--watch'))) {
225+
// A "hack" way to prevent the process from terminating because we don't want to stop it.
226+
return;
227+
}
228+
223229
if (telemetry.isTracking) {
224230
// give telemetry a chance to send events before exit
225231
setTimeout(() => {

0 commit comments

Comments
 (0)