Skip to content

Commit fcd5573

Browse files
authored
feat(cli): add "format" command (#419)
* feat(cli): add "format" command * better-auth: format generated schema * update
1 parent e8f9790 commit fcd5573

File tree

7 files changed

+110
-6
lines changed

7 files changed

+110
-6
lines changed

packages/auth-adapters/better-auth/src/schema-generator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/common-helpers';
2-
import { loadDocument, ZModelCodeGenerator } from '@zenstackhq/language';
2+
import { formatDocument, loadDocument, ZModelCodeGenerator } from '@zenstackhq/language';
33
import {
44
Argument,
55
ArrayExpr,
@@ -111,7 +111,13 @@ async function updateSchema(
111111
}
112112

113113
const generator = new ZModelCodeGenerator();
114-
const content = generator.generate(zmodel);
114+
let content = generator.generate(zmodel);
115+
116+
try {
117+
content = await formatDocument(content);
118+
} catch {
119+
// ignore formatting errors
120+
}
115121

116122
return content;
117123
}

packages/cli/src/actions/format.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { formatDocument } from '@zenstackhq/language';
2+
import colors from 'colors';
3+
import fs from 'node:fs';
4+
import { getSchemaFile } from './action-utils';
5+
6+
type Options = {
7+
schema?: string;
8+
};
9+
10+
/**
11+
* CLI action for formatting a ZModel schema file.
12+
*/
13+
export async function run(options: Options) {
14+
const schemaFile = getSchemaFile(options.schema);
15+
let formattedContent: string;
16+
17+
try {
18+
formattedContent = await formatDocument(fs.readFileSync(schemaFile, 'utf-8'));
19+
} catch (error) {
20+
console.error(colors.red('✗ Schema formatting failed.'));
21+
// Re-throw to maintain CLI exit code behavior
22+
throw error;
23+
}
24+
25+
fs.writeFileSync(schemaFile, formattedContent, 'utf-8');
26+
console.log(colors.green('✓ Schema formatting completed successfully.'));
27+
}

packages/cli/src/actions/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { run as check } from './check';
12
import { run as db } from './db';
3+
import { run as format } from './format';
24
import { run as generate } from './generate';
35
import { run as info } from './info';
46
import { run as init } from './init';
57
import { run as migrate } from './migrate';
6-
import { run as check } from './check';
78

8-
export { db, generate, info, init, migrate, check };
9+
export { check, db, format, generate, info, init, migrate };

packages/cli/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const checkAction = async (options: Parameters<typeof actions.check>[0]): Promis
3030
await telemetry.trackCommand('check', () => actions.check(options));
3131
};
3232

33+
const formatAction = async (options: Parameters<typeof actions.format>[0]): Promise<void> => {
34+
await telemetry.trackCommand('format', () => actions.format(options));
35+
};
36+
3337
function createProgram() {
3438
const program = new Command('zen')
3539
.alias('zenstack')
@@ -145,6 +149,13 @@ function createProgram() {
145149
.addOption(noVersionCheckOption)
146150
.action(checkAction);
147151

152+
program
153+
.command('format')
154+
.description('Format a ZModel schema file')
155+
.addOption(schemaOption)
156+
.addOption(noVersionCheckOption)
157+
.action(formatAction);
158+
148159
program.addHelpCommand('help [command]', 'Display help for a command');
149160

150161
program.hook('preAction', async (_thisCommand, actionCommand) => {

packages/cli/test/format.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createProject, runCli } from './utils';
3+
import fs from 'node:fs';
4+
5+
const model = `
6+
model User {
7+
id String @id @default(cuid())
8+
email String @unique
9+
}
10+
`;
11+
12+
describe('CLI format command test', () => {
13+
it('should format a valid schema successfully', () => {
14+
const workDir = createProject(model);
15+
expect(() => runCli('format', workDir)).not.toThrow();
16+
const updatedContent = fs.readFileSync(`${workDir}/zenstack/schema.zmodel`, 'utf-8');
17+
expect(
18+
updatedContent.includes(`model User {
19+
id String @id @default(cuid())
20+
email String @unique
21+
}`),
22+
).toBeTruthy();
23+
});
24+
25+
it('should silently ignore invalid schema', () => {
26+
const invalidModel = `
27+
model User {
28+
id String @id @default(cuid())
29+
`;
30+
const workDir = createProject(invalidModel);
31+
expect(() => runCli('format', workDir)).not.toThrow();
32+
});
33+
});

packages/language/src/document.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { isAstNode, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium';
1+
import {
2+
isAstNode,
3+
TextDocument,
4+
URI,
5+
type AstNode,
6+
type LangiumDocument,
7+
type LangiumDocuments,
8+
type Mutable,
9+
} from 'langium';
210
import fs from 'node:fs';
311
import path from 'node:path';
412
import { fileURLToPath } from 'node:url';
513
import { isDataSource, type Model } from './ast';
614
import { STD_LIB_MODULE_NAME } from './constants';
715
import { createZModelServices } from './module';
816
import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils';
17+
import type { ZModelFormatter } from './zmodel-formatter';
918

1019
/**
1120
* Loads ZModel document from the given file name. Include the additional document
@@ -200,3 +209,20 @@ function validationAfterImportMerge(model: Model) {
200209
}
201210
return errors;
202211
}
212+
213+
/**
214+
* Formats the given ZModel content.
215+
*/
216+
export async function formatDocument(content: string) {
217+
const services = createZModelServices().ZModelLanguage;
218+
const langiumDocuments = services.shared.workspace.LangiumDocuments;
219+
const document = langiumDocuments.createDocument(URI.parse('memory://schema.zmodel'), content);
220+
const formatter = services.lsp.Formatter as ZModelFormatter;
221+
const identifier = { uri: document.uri.toString() };
222+
const options = formatter.getFormatOptions() ?? {
223+
insertSpaces: true,
224+
tabSize: 4,
225+
};
226+
const edits = await formatter.formatDocument(document, { options, textDocument: identifier });
227+
return TextDocument.applyEdits(document.textDocument, edits);
228+
}

packages/language/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { loadDocument } from './document';
1+
export { formatDocument, loadDocument } from './document';
22
export * from './module';
33
export { ZModelCodeGenerator } from './zmodel-code-generator';

0 commit comments

Comments
 (0)