From 73ed539f6e3f4731b37e3a467f46a6839a71d00e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:05:24 -0800 Subject: [PATCH 1/3] feat(cli): add "format" command --- packages/cli/src/actions/format.ts | 27 ++++++++++++++++++++++++ packages/cli/src/actions/index.ts | 5 +++-- packages/cli/src/index.ts | 11 ++++++++++ packages/cli/test/format.test.ts | 33 ++++++++++++++++++++++++++++++ packages/language/src/document.ts | 28 ++++++++++++++++++++++++- packages/language/src/index.ts | 2 +- 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/actions/format.ts create mode 100644 packages/cli/test/format.test.ts diff --git a/packages/cli/src/actions/format.ts b/packages/cli/src/actions/format.ts new file mode 100644 index 00000000..5cb9b087 --- /dev/null +++ b/packages/cli/src/actions/format.ts @@ -0,0 +1,27 @@ +import { formatDocument } from '@zenstackhq/language'; +import colors from 'colors'; +import fs from 'node:fs'; +import { getSchemaFile } from './action-utils'; + +type Options = { + schema?: string; +}; + +/** + * CLI action for formatting a ZModel schema file. + */ +export async function run(options: Options) { + const schemaFile = getSchemaFile(options.schema); + let formattedContent: string; + + try { + formattedContent = await formatDocument(fs.readFileSync(schemaFile, 'utf-8')); + } catch (error) { + console.error(colors.red('✗ Schema formatting failed.')); + // Re-throw to maintain CLI exit code behavior + throw error; + } + + fs.writeFileSync(schemaFile, formattedContent, 'utf-8'); + console.log(colors.green('✓ Schema formatting completed successfully.')); +} diff --git a/packages/cli/src/actions/index.ts b/packages/cli/src/actions/index.ts index f2238829..f878bac0 100644 --- a/packages/cli/src/actions/index.ts +++ b/packages/cli/src/actions/index.ts @@ -1,8 +1,9 @@ +import { run as check } from './check'; import { run as db } from './db'; +import { run as format } from './format'; import { run as generate } from './generate'; import { run as info } from './info'; import { run as init } from './init'; import { run as migrate } from './migrate'; -import { run as check } from './check'; -export { db, generate, info, init, migrate, check }; +export { check, db, format, generate, info, init, migrate }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 476ac4cf..89eb6fcd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -30,6 +30,10 @@ const checkAction = async (options: Parameters[0]): Promis await telemetry.trackCommand('check', () => actions.check(options)); }; +const formatAction = async (options: Parameters[0]): Promise => { + await telemetry.trackCommand('format', () => actions.format(options)); +}; + function createProgram() { const program = new Command('zen') .alias('zenstack') @@ -145,6 +149,13 @@ function createProgram() { .addOption(noVersionCheckOption) .action(checkAction); + program + .command('format') + .description('Format a ZModel schema file') + .addOption(schemaOption) + .addOption(noVersionCheckOption) + .action(formatAction); + program.addHelpCommand('help [command]', 'Display help for a command'); program.hook('preAction', async (_thisCommand, actionCommand) => { diff --git a/packages/cli/test/format.test.ts b/packages/cli/test/format.test.ts new file mode 100644 index 00000000..9c95960a --- /dev/null +++ b/packages/cli/test/format.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from './utils'; +import fs from 'node:fs'; + +const model = ` +model User { + id String @id @default(cuid()) + email String @unique +} +`; + +describe('CLI format command test', () => { + it('should format a valid schema successfully', () => { + const workDir = createProject(model); + expect(() => runCli('format', workDir)).not.toThrow(); + const updatedContent = fs.readFileSync(`${workDir}/zenstack/schema.zmodel`, 'utf-8'); + expect( + updatedContent.includes(`model User { + id String @id @default(cuid()) + email String @unique +}`), + ).toBeTruthy(); + }); + + it('should silently ignore invalid schema', () => { + const invalidModel = ` +model User { + id String @id @default(cuid()) +`; + const workDir = createProject(invalidModel); + expect(() => runCli('format', workDir)).not.toThrow(); + }); +}); diff --git a/packages/language/src/document.ts b/packages/language/src/document.ts index b8405c48..17146f85 100644 --- a/packages/language/src/document.ts +++ b/packages/language/src/document.ts @@ -1,4 +1,12 @@ -import { isAstNode, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium'; +import { + isAstNode, + TextDocument, + URI, + type AstNode, + type LangiumDocument, + type LangiumDocuments, + type Mutable, +} from 'langium'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -6,6 +14,7 @@ import { isDataSource, type Model } from './ast'; import { STD_LIB_MODULE_NAME } from './constants'; import { createZModelServices } from './module'; import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; +import type { ZModelFormatter } from './zmodel-formatter'; /** * Loads ZModel document from the given file name. Include the additional document @@ -200,3 +209,20 @@ function validationAfterImportMerge(model: Model) { } return errors; } + +/** + * Formats the given ZModel content. + */ +export async function formatDocument(content: string) { + const services = createZModelServices().ZModelLanguage; + const langiumDocuments = services.shared.workspace.LangiumDocuments; + const document = langiumDocuments.createDocument(URI.parse('memory://schema.zmodel'), content); + const formatter = services.lsp.Formatter as ZModelFormatter; + const identifier = { uri: document.uri.toString() }; + const options = formatter.getFormatOptions() ?? { + insertSpaces: true, + tabSize: 4, + }; + const edits = await formatter.formatDocument(document, { options, textDocument: identifier }); + return TextDocument.applyEdits(document.textDocument, edits); +} diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index f11ede5e..4096fb96 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -1,3 +1,3 @@ -export { loadDocument } from './document'; +export { formatDocument, loadDocument } from './document'; export * from './module'; export { ZModelCodeGenerator } from './zmodel-code-generator'; From 0e969f29e07395fd91378a208c230ad8d9cf0b67 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:06:04 -0800 Subject: [PATCH 2/3] better-auth: format generated schema --- .../auth-adapters/better-auth/src/schema-generator.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/auth-adapters/better-auth/src/schema-generator.ts b/packages/auth-adapters/better-auth/src/schema-generator.ts index 78604c48..a2d24f3c 100644 --- a/packages/auth-adapters/better-auth/src/schema-generator.ts +++ b/packages/auth-adapters/better-auth/src/schema-generator.ts @@ -1,5 +1,5 @@ import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/common-helpers'; -import { loadDocument, ZModelCodeGenerator } from '@zenstackhq/language'; +import { formatDocument, loadDocument, ZModelCodeGenerator } from '@zenstackhq/language'; import { Argument, ArrayExpr, @@ -111,7 +111,13 @@ async function updateSchema( } const generator = new ZModelCodeGenerator(); - const content = generator.generate(zmodel); + let content = generator.generate(zmodel); + + try { + content = await formatDocument(content); + } catch (err) { + // ignore formatting errors + } return content; } From 3f0049055eec2df3b3b207821a2e0ed36f444925 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:07:34 -0800 Subject: [PATCH 3/3] update --- packages/auth-adapters/better-auth/src/schema-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth-adapters/better-auth/src/schema-generator.ts b/packages/auth-adapters/better-auth/src/schema-generator.ts index a2d24f3c..e1a3ae78 100644 --- a/packages/auth-adapters/better-auth/src/schema-generator.ts +++ b/packages/auth-adapters/better-auth/src/schema-generator.ts @@ -115,7 +115,7 @@ async function updateSchema( try { content = await formatDocument(content); - } catch (err) { + } catch { // ignore formatting errors }