Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb96362
Merge pull request #478 from zenstackhq/dev
ymc9 Dec 12, 2025
d35b939
Merge pull request #486 from zenstackhq/dev
ymc9 Dec 13, 2025
39fb7d3
Merge pull request #489 from zenstackhq/dev
ymc9 Dec 14, 2025
69dcf6b
Merge pull request #500 from zenstackhq/dev
ymc9 Dec 14, 2025
3f3ffbe
Merge pull request #508 from zenstackhq/dev
ymc9 Dec 16, 2025
da6cf60
Merge pull request #517 from zenstackhq/dev
ymc9 Dec 18, 2025
e371ec9
Merge pull request #521 from zenstackhq/dev
ymc9 Dec 18, 2025
2966af1
Merge pull request #529 from zenstackhq/dev
ymc9 Dec 24, 2025
e71bee7
Merge pull request #532 from zenstackhq/dev
ymc9 Dec 24, 2025
de795f5
Merge pull request #545 from zenstackhq/dev
ymc9 Dec 30, 2025
9b95c29
feat(cli): implement watch mode for generate
DoctorFTB Dec 30, 2025
4895949
chore(root): update pnpm-lock.yaml
DoctorFTB Jan 5, 2026
469cb26
chore(cli): track all model declaration and removed paths, logs in pa…
DoctorFTB Jan 5, 2026
92814ca
fix(cli): typo, unused double array from
DoctorFTB Jan 5, 2026
c9f31ea
fix(orm): preserve zod validation errors when validating custom json …
ymc9 Jan 6, 2026
b625c50
update
ymc9 Jan 6, 2026
3d8f203
Merge branch 'fix/issue-558' into dev
ymc9 Jan 6, 2026
835a01b
chore(cli): move import, fix parallel generation on watch
DoctorFTB Jan 7, 2026
ce76178
Merge branch 'dev' of github.com:DoctorFTB/zenstack-v3 into dev
DoctorFTB Jan 7, 2026
f969ec4
feat(common-helpers): implement single-debounce
DoctorFTB Jan 7, 2026
87a95f0
chore(cli): use single-debounce for debouncing
DoctorFTB Jan 7, 2026
d966e2a
feat(common-helpers): implement single-debounce
DoctorFTB Jan 7, 2026
385e791
fix(common-helpers): re run single-debounce
DoctorFTB Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@zenstackhq/common-helpers": "workspace:*",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"chokidar": "^5.0.0",
"colors": "1.4.0",
"commander": "^8.3.0",
"execa": "^9.6.0",
Expand Down
98 changes: 96 additions & 2 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { invariant } from '@zenstackhq/common-helpers';
import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
import { ZModelLanguageMetaData } from '@zenstackhq/language';
import { isPlugin, isDataModel, type DataModel, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
import { type CliPlugin } from '@zenstackhq/sdk';
import colors from 'colors';
Expand All @@ -16,6 +17,7 @@ type Options = {
schema?: string;
output?: string;
silent: boolean;
watch: boolean;
lite: boolean;
liteOnly: boolean;
};
Expand All @@ -24,6 +26,93 @@ type Options = {
* CLI action for generating code from schema
*/
export async function run(options: Options) {
const model = await pureGenerate(options, false);

if (options.watch) {
const logsEnabled = !options.silent;

if (logsEnabled) {
console.log(colors.green(`\nEnable watch mode!`));
Comment thread
ymc9 marked this conversation as resolved.
Outdated
}

const schemaExtensions = ZModelLanguageMetaData.fileExtensions;

// Get real models file path (cuz its merged into single document -> we need use cst nodes)
const getModelAllPaths = (model: Model) => new Set(
(
model.declarations.filter(
(v) =>
isDataModel(v) &&
Comment thread
ymc9 marked this conversation as resolved.
Outdated
v.$cstNode?.parent?.element.$type === 'Model' &&
!!v.$cstNode.parent.element.$document?.uri?.fsPath,
) as DataModel[]
).map((v) => v.$cstNode!.parent!.element.$document!.uri!.fsPath),
);

const { watch } = await import('chokidar');
Comment thread
ymc9 marked this conversation as resolved.
Outdated

const watchedPaths = getModelAllPaths(model);
let reGenerateSchemaTimeout: ReturnType<typeof setTimeout> | undefined;

if (logsEnabled) {
const logPaths = [...watchedPaths].map((at) => `- ${at}`).join('\n');
console.log(`Watched file paths:\n${logPaths}`);
}

const watcher = watch([...watchedPaths], {
alwaysStat: false,
ignoreInitial: true,
ignorePermissionErrors: true,
ignored: (at) => !schemaExtensions.some((ext) => at.endsWith(ext)),
});

const reGenerateSchema = () => {
clearTimeout(reGenerateSchemaTimeout);

// prevent save multiple files and run multiple times
reGenerateSchemaTimeout = setTimeout(async () => {
Comment thread
ymc9 marked this conversation as resolved.
Outdated
if (logsEnabled) {
console.log('Got changes, run generation!');
}

try {
const newModel = await pureGenerate(options, true);
const allModelsPaths = getModelAllPaths(newModel);
const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at));
Comment thread
ymc9 marked this conversation as resolved.
Outdated

if (newModelPaths.length) {
if (logsEnabled) {
const logPaths = [...newModelPaths].map((at) => `- ${at}`).join('\n');
console.log(`Add file(s) to watch:\n${logPaths}`);
}

newModelPaths.forEach((at) => watchedPaths.add(at));
watcher.add(newModelPaths);
}
} catch (e) {
console.error(e);
}
}, 500);
};

watcher.on('unlink', (pathAt) => {
if (logsEnabled) {
console.log(`Remove file from watch: ${pathAt}`);
Comment thread
ymc9 marked this conversation as resolved.
Outdated
}

watchedPaths.delete(pathAt);
watcher.unwatch(pathAt);

reGenerateSchema();
});

watcher.on('change', () => {
reGenerateSchema();
});
}
}

async function pureGenerate(options: Options, fromWatch: boolean) {
const start = Date.now();

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

if (!options.silent) {
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
console.log(`You can now create a ZenStack client with it.

if (!fromWatch) {
console.log(`You can now create a ZenStack client with it.

\`\`\`ts
import { ZenStackClient } from '@zenstackhq/orm';
Expand All @@ -47,7 +138,10 @@ const client = new ZenStackClient(schema, {
\`\`\`

Check documentation: https://zenstack.dev/docs/`);
}
}

return model;
}

function getOutputPath(options: Options, schemaFile: string) {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function createProgram() {
.addOption(schemaOption)
.addOption(noVersionCheckOption)
.addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
.addOption(new Option('-w, --watch', 'enable watch mode').default(false))
.addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false))
.addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false))
.addOption(new Option('--silent', 'suppress all output except errors').default(false))
Expand Down Expand Up @@ -220,6 +221,11 @@ async function main() {
}
}

if (program.args.includes('generate') && (program.args.includes('-w') || program.args.includes('--watch'))) {
// A "hack" way to prevent the process from terminating because we don't want to stop it.
return;
}

if (telemetry.isTracking) {
// give telemetry a chance to send events before exit
setTimeout(() => {
Expand Down
Loading