feat(cli): add "format" command (#419)

* feat(cli): add "format" command

* better-auth: format generated schema

* update
This commit is contained in:
Yiming Cao 2025-11-17 21:14:25 -08:00 committed by GitHub
parent e8f979067c
commit fcd557312d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 6 deletions

View file

@ -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 {
// ignore formatting errors
}
return content;
}

View file

@ -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.'));
}

View file

@ -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 };

View file

@ -30,6 +30,10 @@ const checkAction = async (options: Parameters<typeof actions.check>[0]): Promis
await telemetry.trackCommand('check', () => actions.check(options));
};
const formatAction = async (options: Parameters<typeof actions.format>[0]): Promise<void> => {
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) => {

View file

@ -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();
});
});

View file

@ -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);
}

View file

@ -1,3 +1,3 @@
export { loadDocument } from './document';
export { formatDocument, loadDocument } from './document';
export * from './module';
export { ZModelCodeGenerator } from './zmodel-code-generator';